-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsend.go
More file actions
130 lines (123 loc) · 3.72 KB
/
Copy pathsend.go
File metadata and controls
130 lines (123 loc) · 3.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package rchan
import (
"context"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// Send delivers v to exactly one receiver. It blocks while the buffer is
// full (capacity > 0) or until pickup (capacity 0, rendezvous).
func (c *Chan[T]) Send(ctx context.Context, v T) error {
data, err := c.opts.codec.Marshal(v)
if err != nil {
return fmt.Errorf("rchan: encode: %w", err)
}
if c.opts.capacity == 0 {
return c.sendRendezvous(ctx, data)
}
return c.sendBuffered(ctx, data)
}
func (c *Chan[T]) trySend(ctx context.Context, data []byte, rz string) (string, error) {
return sendScript.Run(ctx, c.rdb,
[]string{c.keys.meta, c.keys.stream},
c.opts.capacity, data, rz).Text()
}
func (c *Chan[T]) sendBuffered(ctx context.Context, data []byte) error {
res, err := c.trySend(ctx, data, "")
if err != nil {
return fmt.Errorf("rchan: send: %w", err)
}
switch res {
case "CLOSED":
return ErrClosed
case "FULL":
return c.sendBufferedWait(ctx, data)
default:
return nil
}
}
// sendBufferedWait retries the gate until a slot frees. It subscribes to the
// sig channel before re-attempting so a 'space' publish between attempts is
// not missed; a jittered poll is the safety net.
func (c *Chan[T]) sendBufferedWait(ctx context.Context, data []byte) error {
sub := c.rdb.Subscribe(ctx, c.keys.sig)
defer sub.Close()
signals := sub.Channel()
for {
res, err := c.trySend(ctx, data, "")
if err != nil {
return fmt.Errorf("rchan: send: %w", err)
}
switch res {
case "CLOSED":
return ErrClosed
case "FULL":
default:
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-signals: // 'space' or 'close' — either way, retry the gate
case <-time.After(jitter(c.opts.poll)):
}
}
}
// sendRendezvous parks the value with a reply id and blocks until a receiver
// picks it up (LPUSH on the reply key), like an unbuffered chan send.
func (c *Chan[T]) sendRendezvous(ctx context.Context, data []byte) error {
rz := randHex(16)
res, err := c.trySend(ctx, data, rz)
if err != nil {
return fmt.Errorf("rchan: send: %w", err)
}
if res == "CLOSED" {
return ErrClosed
}
entryID := res
rzk := rzKey(c.name, rz)
var waitErr error
for waitErr == nil {
// Short BLPop slices: go-redis blocking commands do not abort on ctx
// cancellation mid-read (ContextTimeoutEnabled is off by default),
// so block 1s at a time and check ctx between slices.
_, err = c.rdb.BLPop(ctx, time.Second, rzk).Result()
if err == nil {
return nil // reply received: rendezvous complete
}
switch {
case !errors.Is(err, redis.Nil) && ctx.Err() == nil:
waitErr = fmt.Errorf("rchan: rendezvous wait: %w", err)
case ctx.Err() != nil:
waitErr = ctx.Err()
default:
// Still parked: make sure the channel still exists. Destroy
// unlinks every key, and nothing would ever push our reply.
if n, eerr := c.rdb.Exists(ctx, c.keys.meta).Result(); eerr == nil && n == 0 {
waitErr = ErrClosed
}
}
}
// Exiting without a reply (cancel, transient error, or destroy): retract,
// unless a receiver already has it — never leave a parked entry behind.
bg, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
retracted, rerr := retractScript.Run(bg, c.rdb,
[]string{c.keys.stream}, group, entryID).Bool()
if rerr != nil {
return errors.Join(waitErr, fmt.Errorf("rchan: retract: %w", rerr))
}
if retracted {
return waitErr
}
if errors.Is(waitErr, ErrClosed) {
// Destroyed: the entry is gone because the stream was unlinked, not
// because a receiver took it — the reply will never arrive.
return ErrClosed
}
// Delivered while we were giving up: the send happened. Drop the reply
// key; a straggling LPUSH dies via its TTL.
c.rdb.Unlink(bg, rzk)
return nil
}