| // Copyright 2013 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package singleflight | |
| import ( | |
| "errors" | |
| "fmt" | |
| "sync" | |
| "sync/atomic" | |
| "testing" | |
| "time" | |
| ) | |
| func TestDo(t *testing.T) { | |
| var g Group | |
| v, err, _ := g.Do("key", func() (any, error) { | |
| return "bar", nil | |
| }) | |
| if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { | |
| t.Errorf("Do = %v; want %v", got, want) | |
| } | |
| if err != nil { | |
| t.Errorf("Do error = %v", err) | |
| } | |
| } | |
| func TestDoErr(t *testing.T) { | |
| var g Group | |
| someErr := errors.New("some error") | |
| v, err, _ := g.Do("key", func() (any, error) { | |
| return nil, someErr | |
| }) | |
| if err != someErr { | |
| t.Errorf("Do error = %v; want someErr %v", err, someErr) | |
| } | |
| if v != nil { | |
| t.Errorf("unexpected non-nil value %#v", v) | |
| } | |
| } | |
| func TestDoDupSuppress(t *testing.T) { | |
| var g Group | |
| var wg1, wg2 sync.WaitGroup | |
| c := make(chan string, 1) | |
| var calls atomic.Int32 | |
| fn := func() (any, error) { | |
| if calls.Add(1) == 1 { | |
| // First invocation. | |
| wg1.Done() | |
| } | |
| v := <-c | |
| c <- v // pump; make available for any future calls | |
| time.Sleep(10 * time.Millisecond) // let more goroutines enter Do | |
| return v, nil | |
| } | |
| const n = 10 | |
| wg1.Add(1) | |
| for i := 0; i < n; i++ { | |
| wg1.Add(1) | |
| wg2.Add(1) | |
| go func() { | |
| defer wg2.Done() | |
| wg1.Done() | |
| v, err, _ := g.Do("key", fn) | |
| if err != nil { | |
| t.Errorf("Do error: %v", err) | |
| return | |
| } | |
| if s, _ := v.(string); s != "bar" { | |
| t.Errorf("Do = %T %v; want %q", v, v, "bar") | |
| } | |
| }() | |
| } | |
| wg1.Wait() | |
| // At least one goroutine is in fn now and all of them have at | |
| // least reached the line before the Do. | |
| c <- "bar" | |
| wg2.Wait() | |
| if got := calls.Load(); got <= 0 || got >= n { | |
| t.Errorf("number of calls = %d; want over 0 and less than %d", got, n) | |
| } | |
| } | |
| func TestForgetUnshared(t *testing.T) { | |
| var g Group | |
| var firstStarted, firstFinished sync.WaitGroup | |
| firstStarted.Add(1) | |
| firstFinished.Add(1) | |
| key := "key" | |
| firstCh := make(chan struct{}) | |
| go func() { | |
| g.Do(key, func() (i any, e error) { | |
| firstStarted.Done() | |
| <-firstCh | |
| return | |
| }) | |
| firstFinished.Done() | |
| }() | |
| firstStarted.Wait() | |
| g.ForgetUnshared(key) // from this point no two function using same key should be executed concurrently | |
| secondCh := make(chan struct{}) | |
| go func() { | |
| g.Do(key, func() (i any, e error) { | |
| // Notify that we started | |
| secondCh <- struct{}{} | |
| <-secondCh | |
| return 2, nil | |
| }) | |
| }() | |
| <-secondCh | |
| resultCh := g.DoChan(key, func() (i any, e error) { | |
| panic("third must not be started") | |
| }) | |
| if g.ForgetUnshared(key) { | |
| t.Errorf("Before first goroutine finished, key %q is shared, should return false", key) | |
| } | |
| close(firstCh) | |
| firstFinished.Wait() | |
| if g.ForgetUnshared(key) { | |
| t.Errorf("After first goroutine finished, key %q is still shared, should return false", key) | |
| } | |
| secondCh <- struct{}{} | |
| if result := <-resultCh; result.Val != 2 { | |
| t.Errorf("We should receive result produced by second call, expected: 2, got %d", result.Val) | |
| } | |
| } | |
| func TestDoAndForgetUnsharedRace(t *testing.T) { | |
| t.Parallel() | |
| var g Group | |
| key := "key" | |
| d := time.Millisecond | |
| for { | |
| var calls, shared atomic.Int64 | |
| const n = 1000 | |
| var wg sync.WaitGroup | |
| wg.Add(n) | |
| for i := 0; i < n; i++ { | |
| go func() { | |
| g.Do(key, func() (any, error) { | |
| time.Sleep(d) | |
| return calls.Add(1), nil | |
| }) | |
| if !g.ForgetUnshared(key) { | |
| shared.Add(1) | |
| } | |
| wg.Done() | |
| }() | |
| } | |
| wg.Wait() | |
| if calls.Load() != 1 { | |
| // The goroutines didn't park in g.Do in time, | |
| // so the key was re-added and may have been shared after the call. | |
| // Try again with more time to park. | |
| d *= 2 | |
| continue | |
| } | |
| // All of the Do calls ended up sharing the first | |
| // invocation, so the key should have been unused | |
| // (and therefore unshared) when they returned. | |
| if shared.Load() > 0 { | |
| t.Errorf("after a single shared Do, ForgetUnshared returned false %d times", shared.Load()) | |
| } | |
| break | |
| } | |
| } |