1
2
3
4
5 package singleflight
6
7 import (
8 "bytes"
9 "errors"
10 "fmt"
11 "os"
12 "os/exec"
13 "runtime"
14 "runtime/debug"
15 "strings"
16 "sync"
17 "sync/atomic"
18 "testing"
19 "time"
20 )
21
22 type errValue struct{}
23
24 func (err *errValue) Error() string {
25 return "error value"
26 }
27
28 func TestPanicErrorUnwrap(t *testing.T) {
29 t.Parallel()
30
31 testCases := []struct {
32 name string
33 panicValue interface{}
34 wrappedErrorType bool
35 }{
36 {
37 name: "panicError wraps non-error type",
38 panicValue: &panicError{value: "string value"},
39 wrappedErrorType: false,
40 },
41 {
42 name: "panicError wraps error type",
43 panicValue: &panicError{value: new(errValue)},
44 wrappedErrorType: false,
45 },
46 }
47
48 for _, tc := range testCases {
49 tc := tc
50
51 t.Run(tc.name, func(t *testing.T) {
52 t.Parallel()
53
54 var recovered interface{}
55
56 group := &Group{}
57
58 func() {
59 defer func() {
60 recovered = recover()
61 t.Logf("after panic(%#v) in group.Do, recovered %#v", tc.panicValue, recovered)
62 }()
63
64 _, _, _ = group.Do(tc.name, func() (interface{}, error) {
65 panic(tc.panicValue)
66 })
67 }()
68
69 if recovered == nil {
70 t.Fatal("expected a non-nil panic value")
71 }
72
73 err, ok := recovered.(error)
74 if !ok {
75 t.Fatalf("recovered non-error type: %T", recovered)
76 }
77
78 if !errors.Is(err, new(errValue)) && tc.wrappedErrorType {
79 t.Errorf("unexpected wrapped error type %T; want %T", err, new(errValue))
80 }
81 })
82 }
83 }
84
85 func TestDo(t *testing.T) {
86 var g Group
87 v, err, _ := g.Do("key", func() (interface{}, error) {
88 return "bar", nil
89 })
90 if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
91 t.Errorf("Do = %v; want %v", got, want)
92 }
93 if err != nil {
94 t.Errorf("Do error = %v", err)
95 }
96 }
97
98 func TestDoErr(t *testing.T) {
99 var g Group
100 someErr := errors.New("Some error")
101 v, err, _ := g.Do("key", func() (interface{}, error) {
102 return nil, someErr
103 })
104 if err != someErr {
105 t.Errorf("Do error = %v; want someErr %v", err, someErr)
106 }
107 if v != nil {
108 t.Errorf("unexpected non-nil value %#v", v)
109 }
110 }
111
112 func TestDoDupSuppress(t *testing.T) {
113 var g Group
114 var wg1, wg2 sync.WaitGroup
115 c := make(chan string, 1)
116 var calls int32
117 fn := func() (interface{}, error) {
118 if atomic.AddInt32(&calls, 1) == 1 {
119
120 wg1.Done()
121 }
122 v := <-c
123 c <- v
124
125 time.Sleep(10 * time.Millisecond)
126
127 return v, nil
128 }
129
130 const n = 10
131 wg1.Add(1)
132 for i := 0; i < n; i++ {
133 wg1.Add(1)
134 wg2.Add(1)
135 go func() {
136 defer wg2.Done()
137 wg1.Done()
138 v, err, _ := g.Do("key", fn)
139 if err != nil {
140 t.Errorf("Do error: %v", err)
141 return
142 }
143 if s, _ := v.(string); s != "bar" {
144 t.Errorf("Do = %T %v; want %q", v, v, "bar")
145 }
146 }()
147 }
148 wg1.Wait()
149
150
151 c <- "bar"
152 wg2.Wait()
153 if got := atomic.LoadInt32(&calls); got <= 0 || got >= n {
154 t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
155 }
156 }
157
158
159
160 func TestForget(t *testing.T) {
161 var g Group
162
163 var (
164 firstStarted = make(chan struct{})
165 unblockFirst = make(chan struct{})
166 firstFinished = make(chan struct{})
167 )
168
169 go func() {
170 g.Do("key", func() (i interface{}, e error) {
171 close(firstStarted)
172 <-unblockFirst
173 close(firstFinished)
174 return
175 })
176 }()
177 <-firstStarted
178 g.Forget("key")
179
180 unblockSecond := make(chan struct{})
181 secondResult := g.DoChan("key", func() (i interface{}, e error) {
182 <-unblockSecond
183 return 2, nil
184 })
185
186 close(unblockFirst)
187 <-firstFinished
188
189 thirdResult := g.DoChan("key", func() (i interface{}, e error) {
190 return 3, nil
191 })
192
193 close(unblockSecond)
194 <-secondResult
195 r := <-thirdResult
196 if r.Val != 2 {
197 t.Errorf("We should receive result produced by second call, expected: 2, got %d", r.Val)
198 }
199 }
200
201 func TestDoChan(t *testing.T) {
202 var g Group
203 ch := g.DoChan("key", func() (interface{}, error) {
204 return "bar", nil
205 })
206
207 res := <-ch
208 v := res.Val
209 err := res.Err
210 if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
211 t.Errorf("Do = %v; want %v", got, want)
212 }
213 if err != nil {
214 t.Errorf("Do error = %v", err)
215 }
216 }
217
218
219
220 func TestPanicDo(t *testing.T) {
221 var g Group
222 fn := func() (interface{}, error) {
223 panic("invalid memory address or nil pointer dereference")
224 }
225
226 const n = 5
227 waited := int32(n)
228 panicCount := int32(0)
229 done := make(chan struct{})
230 for i := 0; i < n; i++ {
231 go func() {
232 defer func() {
233 if err := recover(); err != nil {
234 t.Logf("Got panic: %v\n%s", err, debug.Stack())
235 atomic.AddInt32(&panicCount, 1)
236 }
237
238 if atomic.AddInt32(&waited, -1) == 0 {
239 close(done)
240 }
241 }()
242
243 g.Do("key", fn)
244 }()
245 }
246
247 select {
248 case <-done:
249 if panicCount != n {
250 t.Errorf("Expect %d panic, but got %d", n, panicCount)
251 }
252 case <-time.After(time.Second):
253 t.Fatalf("Do hangs")
254 }
255 }
256
257 func TestGoexitDo(t *testing.T) {
258 var g Group
259 fn := func() (interface{}, error) {
260 runtime.Goexit()
261 return nil, nil
262 }
263
264 const n = 5
265 waited := int32(n)
266 done := make(chan struct{})
267 for i := 0; i < n; i++ {
268 go func() {
269 var err error
270 defer func() {
271 if err != nil {
272 t.Errorf("Error should be nil, but got: %v", err)
273 }
274 if atomic.AddInt32(&waited, -1) == 0 {
275 close(done)
276 }
277 }()
278 _, err, _ = g.Do("key", fn)
279 }()
280 }
281
282 select {
283 case <-done:
284 case <-time.After(time.Second):
285 t.Fatalf("Do hangs")
286 }
287 }
288
289 func executable(t testing.TB) string {
290 exe, err := os.Executable()
291 if err != nil {
292 t.Skipf("skipping: test executable not found")
293 }
294
295
296
297 cmd := exec.Command(exe, "-test.list=^$")
298 cmd.Env = []string{}
299 if err := cmd.Run(); err != nil {
300 t.Skipf("skipping: exec appears not to work on %s: %v", runtime.GOOS, err)
301 }
302
303 return exe
304 }
305
306 func TestPanicDoChan(t *testing.T) {
307 if os.Getenv("TEST_PANIC_DOCHAN") != "" {
308 defer func() {
309 recover()
310 }()
311
312 g := new(Group)
313 ch := g.DoChan("", func() (interface{}, error) {
314 panic("Panicking in DoChan")
315 })
316 <-ch
317 t.Fatalf("DoChan unexpectedly returned")
318 }
319
320 t.Parallel()
321
322 cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
323 cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
324 out := new(bytes.Buffer)
325 cmd.Stdout = out
326 cmd.Stderr = out
327 if err := cmd.Start(); err != nil {
328 t.Fatal(err)
329 }
330
331 err := cmd.Wait()
332 t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
333 if err == nil {
334 t.Errorf("Test subprocess passed; want a crash due to panic in DoChan")
335 }
336 if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
337 t.Errorf("Test subprocess failed with an unexpected failure mode.")
338 }
339 if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) {
340 t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan")
341 }
342 }
343
344 func TestPanicDoSharedByDoChan(t *testing.T) {
345 if os.Getenv("TEST_PANIC_DOCHAN") != "" {
346 blocked := make(chan struct{})
347 unblock := make(chan struct{})
348
349 g := new(Group)
350 go func() {
351 defer func() {
352 recover()
353 }()
354 g.Do("", func() (interface{}, error) {
355 close(blocked)
356 <-unblock
357 panic("Panicking in Do")
358 })
359 }()
360
361 <-blocked
362 ch := g.DoChan("", func() (interface{}, error) {
363 panic("DoChan unexpectedly executed callback")
364 })
365 close(unblock)
366 <-ch
367 t.Fatalf("DoChan unexpectedly returned")
368 }
369
370 t.Parallel()
371
372 cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
373 cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
374 out := new(bytes.Buffer)
375 cmd.Stdout = out
376 cmd.Stderr = out
377 if err := cmd.Start(); err != nil {
378 t.Fatal(err)
379 }
380
381 err := cmd.Wait()
382 t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
383 if err == nil {
384 t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan")
385 }
386 if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
387 t.Errorf("Test subprocess failed with an unexpected failure mode.")
388 }
389 if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) {
390 t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
391 }
392 }
393
394 func ExampleGroup() {
395 g := new(Group)
396
397 block := make(chan struct{})
398 res1c := g.DoChan("key", func() (interface{}, error) {
399 <-block
400 return "func 1", nil
401 })
402 res2c := g.DoChan("key", func() (interface{}, error) {
403 <-block
404 return "func 2", nil
405 })
406 close(block)
407
408 res1 := <-res1c
409 res2 := <-res2c
410
411
412 fmt.Println("Shared:", res2.Shared)
413
414
415 fmt.Println("Equal results:", res1.Val.(string) == res2.Val.(string))
416 fmt.Println("Result:", res1.Val)
417
418
419
420
421
422 }
423
View as plain text