Repository navigation
Expand file tree
/
Copy pathrendezvous_test.go
More file actions
226 lines (216 loc) · 6.33 KB
/
Copy pathrendezvous_test.go
File metadata and controls
226 lines (216 loc) · 6.33 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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package rchan
import (
"context"
"errors"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
func TestRendezvousHandoff(t *testing.T) {
ctx := context.Background()
rdb := testClient(t)
ch, err := New[string](ctx, rdb, testName(t, rdb))
if err != nil {
t.Fatal(err)
}
defer ch.Stop()
done := make(chan error, 1)
go func() { done <- ch.Send(ctx, "v") }()
select {
case err := <-done:
t.Fatalf("rendezvous send must block until pickup, returned %v", err)
case <-time.After(200 * time.Millisecond):
}
rctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
v, ok, err := ch.Recv(rctx)
if err != nil || !ok || v != "v" {
t.Fatalf("v=%q ok=%v err=%v", v, ok, err)
}
select {
case err := <-done:
if err != nil {
t.Fatalf("sender did not complete cleanly: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("sender still blocked after pickup")
}
}
func TestRendezvousCancelRetracts(t *testing.T) {
rdb := testClient(t)
name := testName(t, rdb)
ch, _ := New[string](context.Background(), rdb, name)
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
if err := ch.Send(ctx, "v"); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("want DeadlineExceeded, got %v", err)
}
n, _ := rdb.XLen(context.Background(), "rchan:{"+name+"}:s").Result()
if n != 0 {
t.Fatalf("XLEN=%d, want 0 (entry retracted)", n)
}
}
func TestRetractLosesToDelivery(t *testing.T) {
// Script-level check of the race branch: once an entry is in the PEL,
// retract must refuse so the sender treats the value as delivered.
ctx := context.Background()
rdb := testClient(t)
name := testName(t, rdb)
ch, _ := New[string](ctx, rdb, name)
_ = ch
stream := "rchan:{" + name + "}:s"
id, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: stream, Values: map[string]any{"v": `"x"`, "rz": "r1"},
}).Result()
if err != nil {
t.Fatal(err)
}
// Deliver it (into the PEL) with a raw consumer.
if err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "recv", Consumer: "rogue", Streams: []string{stream, ">"},
Count: 1, Block: -1,
}).Err(); err != nil {
t.Fatal(err)
}
got, err := retractScript.Run(ctx, rdb, []string{stream}, "recv", id).Bool()
if err != nil {
t.Fatal(err)
}
if got {
t.Fatal("retract must fail for a delivered (PEL) entry")
}
}
func TestRendezvousSurvivesReceiverCrash(t *testing.T) {
ctx := context.Background()
rdb := testClient(t)
name := testName(t, rdb)
vis := 200 * time.Millisecond
ch, _ := New[string](ctx, rdb, name, WithVisibilityTimeout(vis))
defer ch.Stop()
stream := "rchan:{" + name + "}:s"
done := make(chan error, 1)
go func() { done <- ch.Send(ctx, "v") }()
// Rogue consumer picks the entry up and "crashes" before replying.
eventually(t, 2*time.Second, "entry parked", func() bool {
n, _ := rdb.XLen(ctx, stream).Result()
return n == 1
})
if err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "recv", Consumer: "rogue", Streams: []string{stream, ">"},
Count: 1, Block: -1,
}).Err(); err != nil {
t.Fatal(err)
}
// A healthy receiving handle reclaims it and completes the rendezvous.
rch, _ := New[string](ctx, rdb, name, WithVisibilityTimeout(vis))
defer rch.Stop()
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
v, ok, err := rch.Recv(rctx)
if err != nil || !ok || v != "v" {
t.Fatalf("v=%q ok=%v err=%v", v, ok, err)
}
select {
case err := <-done:
if err != nil {
t.Fatalf("sender should complete after reclaim, got %v", err)
}
case <-time.After(10 * time.Second):
t.Fatal("sender still blocked; reclaim did not send rendezvous reply")
}
}
func TestRendezvousSenderUnblocksOnDestroy(t *testing.T) {
ctx := context.Background()
rdb := testClient(t)
name := testName(t, rdb)
ch, err := New[string](ctx, rdb, name)
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- ch.Send(ctx, "doomed") }()
eventually(t, 2*time.Second, "entry parked", func() bool {
n, _ := rdb.XLen(ctx, "rchan:{"+name+"}:s").Result()
return n == 1
})
if err := Destroy(ctx, rdb, name); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
if !errors.Is(err, ErrClosed) {
t.Fatalf("want ErrClosed after destroy, got %v", err)
}
case <-time.After(10 * time.Second):
t.Fatal("rendezvous sender still parked after Destroy")
}
}
func TestRendezvousWaitsForLateReceiver(t *testing.T) {
ctx := context.Background()
rdb := testClient(t)
ch, err := New[string](ctx, rdb, testName(t, rdb))
if err != nil {
t.Fatal(err)
}
defer ch.Stop()
done := make(chan error, 1)
go func() { done <- ch.Send(ctx, "patience") }()
time.Sleep(2500 * time.Millisecond) // longer than two BLPop slices
select {
case err := <-done:
t.Fatalf("send gave up while no receiver present: %v", err)
default:
}
rctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
v, ok, err := ch.Recv(rctx)
if err != nil || !ok || v != "patience" {
t.Fatalf("v=%q ok=%v err=%v", v, ok, err)
}
if err := <-done; err != nil {
t.Fatalf("sender error after late pickup: %v", err)
}
}
// TestRendezvousParkedSenderDrainsAfterClose: Close does not wake a parked
// rendezvous sender — its value stays drainable, a receiver completes the
// handoff, and only then is the channel observed as closed.
func TestRendezvousParkedSenderDrainsAfterClose(t *testing.T) {
ctx := context.Background()
rdb := testClient(t)
name := testName(t, rdb)
ch, err := New[string](ctx, rdb, name)
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- ch.Send(ctx, "parked") }()
eventually(t, 2*time.Second, "entry parked", func() bool {
n, _ := rdb.XLen(ctx, "rchan:{"+name+"}:s").Result()
return n == 1
})
if err := ch.Close(ctx); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
t.Fatalf("close must not wake a parked rendezvous sender, got %v", err)
case <-time.After(1500 * time.Millisecond):
}
rch, err := New[string](ctx, rdb, name)
if err != nil {
t.Fatal(err)
}
defer rch.Stop()
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
v, ok, err := rch.Recv(rctx)
if err != nil || !ok || v != "parked" {
t.Fatalf("v=%q ok=%v err=%v", v, ok, err)
}
if err := <-done; err != nil {
t.Fatalf("sender should complete after drain, got %v", err)
}
if _, ok, err := rch.Recv(rctx); ok || err != nil {
t.Fatalf("after drain: ok=%v err=%v, want closed", ok, err)
}
}