Skip to content

Commit

Permalink
Ensure no function calls after one context is cancelled
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Aug 23, 2023
1 parent f856ab8 commit db682af
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: '1.21'

- name: Checkout
uses: actions/checkout@v1
Expand All @@ -34,7 +34,7 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.40.1
version: v1.54.2
args: --timeout 10m

- name: Vet
Expand Down
11 changes: 1 addition & 10 deletions singleflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

// Package singleflight provides a duplicate function call suppression
// mechanism similar to golang.org/x/sync/singleflight with support
// for context cancelation.
// for context cancellation.
package singleflight

import (
Expand Down Expand Up @@ -76,8 +76,6 @@ func (g *Group) wait(ctx context.Context, key string, c *call) (v interface{}, s
c.counter--
if c.counter == 0 {
c.cancel()
}
if !c.forgotten {
delete(g.calls, key)
}
g.mu.Unlock()
Expand All @@ -89,9 +87,6 @@ func (g *Group) wait(ctx context.Context, key string, c *call) (v interface{}, s
// an earlier call to complete.
func (g *Group) Forget(key string) {
g.mu.Lock()
if c, ok := g.calls[key]; ok {
c.forgotten = true
}
delete(g.calls, key)
g.mu.Unlock()
}
Expand All @@ -105,10 +100,6 @@ type call struct {
// done channel signals that the function call is done.
done chan struct{}

// forgotten indicates whether Forget was called with this call's key
// while the call was still in flight.
forgotten bool

// shared indicates if results val and err are passed to multiple callers.
shared bool

Expand Down
52 changes: 52 additions & 0 deletions singleflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,58 @@ func TestDo_cancelContextSecond(t *testing.T) {
}
}

func TestDo_callDoAfterCancellation(t *testing.T) {
done := make(chan struct{})
defer close(done)

var g singleflight.Group

callCounter := new(atomic.Uint64)
fn := func(_ context.Context) (interface{}, error) {
callCounter.Add(1)
select {
case <-time.After(time.Second):
case <-done:
}
return "", nil
}

go func() {
// keep the function call active for long period (1 second)
if _, _, err := g.Do(context.Background(), "key", fn); err != nil {
panic(err)
}
}()

{ // make another call that is canceled shortly (100 milliseconds)
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
_, _, err := g.Do(ctx, "key", fn)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
}

want := uint64(1)

if got := callCounter.Load(); got != want {
t.Errorf("got call counter %v, want %v", got, want)
}

{ // make another call after the previous call cancellation
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
_, _, err := g.Do(ctx, "key", fn)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
}

if got := callCounter.Load(); got != want {
t.Errorf("got call counter %v, want %v", got, want)
}
}

func TestForget(t *testing.T) {
done := make(chan struct{})
defer close(done)
Expand Down

0 comments on commit db682af

Please sign in to comment.