-
Notifications
You must be signed in to change notification settings - Fork 253
Expand file tree
/
Copy pathsingleflight_test.go
More file actions
338 lines (310 loc) · 8.92 KB
/
Copy pathsingleflight_test.go
File metadata and controls
338 lines (310 loc) · 8.92 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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package rueidis
import (
"context"
"errors"
"runtime"
"sync/atomic"
"testing"
"time"
)
func TestSingleFlight(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
var calls, done, err int64
sg := call{}
for range 1000 {
go func() {
if ret := sg.Do(context.Background(), func() error {
atomic.AddInt64(&calls, 1)
// wait for all goroutine invoked then return
for sg.suppressing() != 1000 {
runtime.Gosched()
}
return errors.New("I should be the only ret")
}); ret != nil {
atomic.AddInt64(&err, 1)
}
atomic.AddInt64(&done, 1)
}()
}
for atomic.LoadInt64(&done) != 1000 {
runtime.Gosched()
}
if atomic.LoadInt64(&calls) == 0 {
t.Fatalf("singleflight not call at all")
}
if v := atomic.LoadInt64(&calls); v != 1 {
t.Fatalf("singleflight should suppress all concurrent calls, got: %v", v)
}
// Every caller must see the error: the one that ran fn and everyone who
// waited on it. Waiters used to get nil, and nil looks like success to
// code that retries until an operation succeeds.
if v := atomic.LoadInt64(&err); v != 1000 {
t.Fatalf("all callers should get the error of the run they waited on, got: %v", v)
}
}
// TestSingleFlightJoinerReceivesFlightError: a caller that waits for an
// already-running fn must get the error that fn actually returned.
//
// It used to get nil even when fn failed. nil looks like success, so code that
// retries an operation until it succeeds stopped retrying after a run that
// failed: sentinelClient.refreshRetry loops until refresh() returns nil, so
// joining someone else's failed refresh ended the retry loop with the master
// still unresolved. The test also checks the other direction: the error of a
// finished run must not leak into the next run.
func TestSingleFlightJoinerReceivesFlightError(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
block := make(chan struct{})
flightErr := errors.New("flight failed")
sg := call{}
initiatorDone := make(chan error, 1)
go func() {
initiatorDone <- sg.Do(context.Background(), func() error {
<-block
return flightErr
})
}()
for sg.suppressing() != 1 {
runtime.Gosched()
}
joinerDone := make(chan error, 1)
go func() {
joinerDone <- sg.Do(context.Background(), func() error {
t.Error("joiner fn must not run")
return nil
})
}()
for sg.suppressing() != 2 {
runtime.Gosched()
}
close(block)
if err := <-initiatorDone; err != flightErr {
t.Fatalf("initiator: unexpected err %v", err)
}
if err := <-joinerDone; err != flightErr {
t.Fatalf("joiner: unexpected err %v", err)
}
// A caller arriving after the flight completed starts a fresh flight and
// gets its own result, not the previous flight's error.
if err := sg.Do(context.Background(), func() error { return nil }); err != nil {
t.Fatalf("fresh flight: unexpected err %v", err)
}
}
// TestSingleFlightJoinerKeepsItsOwnFlightError: a waiter must get the error of
// the flight it waited on even when the next flight overlaps with it.
//
// The call struct is reused. do() clears the flight before it closes the
// channel, so the next flight can start while the previous waiters are still
// waking up. Anything per-flight kept on call itself is overwritten in that
// window, and the waiters then read the next flight's result instead of their
// own — nil, which is the failure this fix is about. Keeping err on the flight
// avoids it, and also removes the need to synchronize the read: the write
// happens before close(ch), and the read happens after <-ch.
func TestSingleFlightJoinerKeepsItsOwnFlightError(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
flightErr := errors.New("flight failed")
const joiners = 50
for range 200 {
var (
sg = call{}
block = make(chan struct{})
errs = make([]error, joiners)
done int64
)
go func() {
sg.Do(context.Background(), func() error {
<-block
return flightErr
})
}()
for sg.suppressing() != 1 {
runtime.Gosched()
}
for j := range joiners {
go func(j int) {
errs[j] = sg.Do(context.Background(), func() error { return nil })
atomic.AddInt64(&done, 1)
}(j)
}
for sg.suppressing() != joiners+1 {
runtime.Gosched()
}
// The next flight starts as soon as the current one clears its
// counter, which is before the waiters are released.
next := make(chan struct{})
go func() {
defer close(next)
for sg.suppressing() != 0 {
runtime.Gosched()
}
sg.Do(context.Background(), func() error { return nil })
}()
close(block)
for atomic.LoadInt64(&done) != joiners {
runtime.Gosched()
}
<-next
for j := range errs {
if errs[j] != flightErr {
t.Fatalf("joiner %v got %v, want %v", j, errs[j], flightErr)
}
}
}
}
// TestSingleFlightCancellableJoinerAtCompletion drives callers into the moment
// a flight completes, half of them able to be cancelled and half not, since the
// two take different branches of Do.
//
// do() clears c.fl before it releases the waiters, so a caller arriving in that
// window finds no flight and starts its own instead of joining. Whichever side
// of it a caller lands on, it must come back with the error of the flight it
// waited on, and none may stay parked.
func TestSingleFlightCancellableJoinerAtCompletion(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
flightErr := errors.New("flight failed")
const callers = 20
for range 200 {
var (
sg call
done int64
results = make([]error, callers)
)
for j := range callers {
go func(j int) {
ctx := context.Background()
if j%2 == 0 { // half wait through the ctx branch of Do
c, cancel := context.WithCancel(ctx)
defer cancel()
ctx = c
}
results[j] = sg.Do(ctx, func() error {
runtime.Gosched()
return flightErr
})
atomic.AddInt64(&done, 1)
}(j)
}
for atomic.LoadInt64(&done) != callers {
runtime.Gosched()
}
for j := range results {
if results[j] != flightErr {
t.Fatalf("caller %v got %v, want %v", j, results[j], flightErr)
}
}
}
}
// TestSingleFlightCancelledJoinerAtCompletion: the same window, with the
// cancellable waiters cancelled around the time the flight ends. Each must come
// back with either its flight's error or the cancellation, never nil and never
// parked, and the runner must still release the waiters that stayed.
func TestSingleFlightCancelledJoinerAtCompletion(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
flightErr := errors.New("flight failed")
const callers = 20
for range 200 {
var (
sg call
done int64
release = make(chan struct{})
results = make([]error, callers)
)
for j := range callers {
go func(j int) {
ctx, cancel := context.WithCancel(context.Background())
if j%2 == 0 {
defer cancel()
} else {
cancel() // already cancelled on arrival
}
results[j] = sg.Do(ctx, func() error {
<-release
return flightErr
})
atomic.AddInt64(&done, 1)
}(j)
}
close(release)
for atomic.LoadInt64(&done) != callers {
runtime.Gosched()
}
for j := range results {
if results[j] != flightErr && results[j] != context.Canceled {
t.Fatalf("caller %v got %v", j, results[j])
}
}
}
}
func TestSingleFlightWithContext(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
ch := make(chan struct{})
sg := call{}
go func() {
sg.Do(context.Background(), func() error {
<-ch
return nil
})
}()
for sg.suppressing() != 1 {
time.Sleep(time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := sg.Do(ctx, func() error { return nil }); err != context.Canceled {
t.Fatalf("unexpected err %v", err)
}
go func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := sg.Do(ctx, func() error { return nil }); err != nil {
t.Errorf("unexpected err %v", err)
}
}()
for sg.suppressing() != 3 {
time.Sleep(time.Millisecond)
}
close(ch)
if err := sg.Do(context.Background(), func() error { return nil }); err != nil {
t.Fatalf("unexpected err %v", err)
}
}
func TestSingleFlightDelayDoDedupesInFlight(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
ch := make(chan struct{})
sg := call{}
sg.DelayDo(0, func() error {
<-ch
return nil
})
cn := 0
sg.DelayDo(0, func() error {
cn++ // dedupe: should not run while first is in-flight
return nil
})
if cn != 0 {
t.Fatalf("DelayDo did not dedupe, cn=%v", cn)
}
if sc := sg.suppressing(); sc != 1 {
t.Fatalf("unexpected suppressing %v", sc)
}
close(ch)
}
func TestSingleFlightDelayDoHonorsDelay(t *testing.T) {
defer ShouldNotLeak(SetupLeakDetection())
sg := call{}
delay := 75 * time.Millisecond
start := time.Now()
done := make(chan time.Time, 1)
sg.DelayDo(delay, func() error {
done <- time.Now()
return nil
})
select {
case ts := <-done:
got := ts.Sub(start)
if got < delay {
t.Fatalf("DelayDo ran too early: waited %v, expected >= %v", got, delay)
}
case <-time.After(time.Second):
t.Fatalf("DelayDo never ran")
}
}