Skip to content

Commit db682af

Browse files
committed
Ensure no function calls after one context is cancelled
1 parent f856ab8 commit db682af

3 files changed

Lines changed: 55 additions & 12 deletions

File tree

.github/workflows/go.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Set up Go
1515
uses: actions/setup-go@v2
1616
with:
17-
go-version: 1.16
17+
go-version: '1.21'
1818

1919
- name: Checkout
2020
uses: actions/checkout@v1
@@ -34,7 +34,7 @@ jobs:
3434
- name: Lint
3535
uses: golangci/golangci-lint-action@v2
3636
with:
37-
version: v1.40.1
37+
version: v1.54.2
3838
args: --timeout 10m
3939

4040
- name: Vet

singleflight.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
// Package singleflight provides a duplicate function call suppression
77
// mechanism similar to golang.org/x/sync/singleflight with support
8-
// for context cancelation.
8+
// for context cancellation.
99
package singleflight
1010

1111
import (
@@ -76,8 +76,6 @@ func (g *Group) wait(ctx context.Context, key string, c *call) (v interface{}, s
7676
c.counter--
7777
if c.counter == 0 {
7878
c.cancel()
79-
}
80-
if !c.forgotten {
8179
delete(g.calls, key)
8280
}
8381
g.mu.Unlock()
@@ -89,9 +87,6 @@ func (g *Group) wait(ctx context.Context, key string, c *call) (v interface{}, s
8987
// an earlier call to complete.
9088
func (g *Group) Forget(key string) {
9189
g.mu.Lock()
92-
if c, ok := g.calls[key]; ok {
93-
c.forgotten = true
94-
}
9590
delete(g.calls, key)
9691
g.mu.Unlock()
9792
}
@@ -105,10 +100,6 @@ type call struct {
105100
// done channel signals that the function call is done.
106101
done chan struct{}
107102

108-
// forgotten indicates whether Forget was called with this call's key
109-
// while the call was still in flight.
110-
forgotten bool
111-
112103
// shared indicates if results val and err are passed to multiple callers.
113104
shared bool
114105

singleflight_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,58 @@ func TestDo_cancelContextSecond(t *testing.T) {
201201
}
202202
}
203203

204+
func TestDo_callDoAfterCancellation(t *testing.T) {
205+
done := make(chan struct{})
206+
defer close(done)
207+
208+
var g singleflight.Group
209+
210+
callCounter := new(atomic.Uint64)
211+
fn := func(_ context.Context) (interface{}, error) {
212+
callCounter.Add(1)
213+
select {
214+
case <-time.After(time.Second):
215+
case <-done:
216+
}
217+
return "", nil
218+
}
219+
220+
go func() {
221+
// keep the function call active for long period (1 second)
222+
if _, _, err := g.Do(context.Background(), "key", fn); err != nil {
223+
panic(err)
224+
}
225+
}()
226+
227+
{ // make another call that is canceled shortly (100 milliseconds)
228+
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
229+
defer cancel()
230+
_, _, err := g.Do(ctx, "key", fn)
231+
if !errors.Is(err, context.DeadlineExceeded) {
232+
t.Fatal(err)
233+
}
234+
}
235+
236+
want := uint64(1)
237+
238+
if got := callCounter.Load(); got != want {
239+
t.Errorf("got call counter %v, want %v", got, want)
240+
}
241+
242+
{ // make another call after the previous call cancellation
243+
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
244+
defer cancel()
245+
_, _, err := g.Do(ctx, "key", fn)
246+
if !errors.Is(err, context.DeadlineExceeded) {
247+
t.Fatal(err)
248+
}
249+
}
250+
251+
if got := callCounter.Load(); got != want {
252+
t.Errorf("got call counter %v, want %v", got, want)
253+
}
254+
}
255+
204256
func TestForget(t *testing.T) {
205257
done := make(chan struct{})
206258
defer close(done)

0 commit comments

Comments
 (0)