Skip to content

Commit 56d3577

Browse files
cuonglmbradfitz
authored andcommitted
singleflight: fix duplicate deleting key when Forget called
When Forget was called, we delete key associated with current call from map. When that call is done, it does delete key again, causing the same key set by other call after Forget lost. To fix it, adding a boolean value to check whether the call is forgotten, the call only does delete key if Forget is not called. Fixes golang/go#31420 Change-Id: I9708352ca3ff76c77f659916b37a496fdeb480d2 Reviewed-on: https://go-review.googlesource.com/c/sync/+/171897 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1 parent e225da7 commit 56d3577

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

singleflight/singleflight.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ type call struct {
1717
val interface{}
1818
err error
1919

20+
// forgotten indicates whether Forget was called with this call's key
21+
// while the call was still in flight.
22+
forgotten bool
23+
2024
// These fields are read and written with the singleflight
2125
// mutex held before the WaitGroup is done, and are read but
2226
// not written after the WaitGroup is done.
@@ -94,7 +98,9 @@ func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
9498
c.wg.Done()
9599

96100
g.mu.Lock()
97-
delete(g.m, key)
101+
if !c.forgotten {
102+
delete(g.m, key)
103+
}
98104
for _, ch := range c.chans {
99105
ch <- Result{c.val, c.err, c.dups > 0}
100106
}
@@ -106,6 +112,9 @@ func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
106112
// an earlier call to complete.
107113
func (g *Group) Forget(key string) {
108114
g.mu.Lock()
115+
if c, ok := g.m[key]; ok {
116+
c.forgotten = true
117+
}
109118
delete(g.m, key)
110119
g.mu.Unlock()
111120
}

singleflight/singleflight_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,75 @@ func TestDoDupSuppress(t *testing.T) {
8585
t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
8686
}
8787
}
88+
89+
// Test that singleflight behaves correctly after Forget called.
90+
// See https://github.com/golang/go/issues/31420
91+
func TestForget(t *testing.T) {
92+
var g Group
93+
94+
var firstStarted, firstFinished sync.WaitGroup
95+
96+
firstStarted.Add(1)
97+
firstFinished.Add(1)
98+
99+
firstCh := make(chan struct{})
100+
go func() {
101+
g.Do("key", func() (i interface{}, e error) {
102+
firstStarted.Done()
103+
<-firstCh
104+
firstFinished.Done()
105+
return
106+
})
107+
}()
108+
109+
firstStarted.Wait()
110+
g.Forget("key") // from this point no two function using same key should be executed concurrently
111+
112+
var secondStarted int32
113+
var secondFinished int32
114+
var thirdStarted int32
115+
116+
secondCh := make(chan struct{})
117+
secondRunning := make(chan struct{})
118+
go func() {
119+
g.Do("key", func() (i interface{}, e error) {
120+
defer func() {
121+
}()
122+
atomic.AddInt32(&secondStarted, 1)
123+
// Notify that we started
124+
secondCh <- struct{}{}
125+
// Wait other get above signal
126+
<-secondRunning
127+
<-secondCh
128+
atomic.AddInt32(&secondFinished, 1)
129+
return 2, nil
130+
})
131+
}()
132+
133+
close(firstCh)
134+
firstFinished.Wait() // wait for first execution (which should not affect execution after Forget)
135+
136+
<-secondCh
137+
// Notify second that we got the signal that it started
138+
secondRunning <- struct{}{}
139+
if atomic.LoadInt32(&secondStarted) != 1 {
140+
t.Fatal("Second execution should be executed due to usage of forget")
141+
}
142+
143+
if atomic.LoadInt32(&secondFinished) == 1 {
144+
t.Fatal("Second execution should be still active")
145+
}
146+
147+
close(secondCh)
148+
result, _, _ := g.Do("key", func() (i interface{}, e error) {
149+
atomic.AddInt32(&thirdStarted, 1)
150+
return 3, nil
151+
})
152+
153+
if atomic.LoadInt32(&thirdStarted) != 0 {
154+
t.Error("Third call should not be started because was started during second execution")
155+
}
156+
if result != 2 {
157+
t.Errorf("We should receive result produced by second call, expected: 2, got %d", result)
158+
}
159+
}

0 commit comments

Comments
 (0)