Skip to content

Commit 9612d61

Browse files
committed
chore(pkg): update singleflight
1 parent 92f396d commit 9612d61

File tree

2 files changed

+127
-24
lines changed

2 files changed

+127
-24
lines changed

pkg/singleflight/signleflight_test.go

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package singleflight
5+
package singleflight // import "golang.org/x/sync/singleflight"
66

77
import (
88
"bytes"
@@ -19,6 +19,68 @@ import (
1919
"time"
2020
)
2121

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 any
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+
for _, tc := range testCases {
48+
tc := tc
49+
50+
t.Run(tc.name, func(t *testing.T) {
51+
t.Parallel()
52+
53+
var recovered any
54+
55+
group := &Group[any]{}
56+
57+
func() {
58+
defer func() {
59+
recovered = recover()
60+
t.Logf("after panic(%#v) in group.Do, recovered %#v", tc.panicValue, recovered)
61+
}()
62+
63+
_, _, _ = group.Do(tc.name, func() (any, error) {
64+
panic(tc.panicValue)
65+
})
66+
}()
67+
68+
if recovered == nil {
69+
t.Fatal("expected a non-nil panic value")
70+
}
71+
72+
err, ok := recovered.(error)
73+
if !ok {
74+
t.Fatalf("recovered non-error type: %T", recovered)
75+
}
76+
77+
if !errors.Is(err, new(errValue)) && tc.wrappedErrorType {
78+
t.Errorf("unexpected wrapped error type %T; want %T", err, new(errValue))
79+
}
80+
})
81+
}
82+
}
83+
2284
func TestDo(t *testing.T) {
2385
var g Group[string]
2486
v, err, _ := g.Do("key", func() (string, error) {
@@ -95,7 +157,7 @@ func TestDoDupSuppress(t *testing.T) {
95157
// Test that singleflight behaves correctly after Forget called.
96158
// See https://github.com/golang/go/issues/31420
97159
func TestForget(t *testing.T) {
98-
var g Group[any]
160+
var g Group[int]
99161

100162
var (
101163
firstStarted = make(chan struct{})
@@ -104,7 +166,7 @@ func TestForget(t *testing.T) {
104166
)
105167

106168
go func() {
107-
g.Do("key", func() (i any, e error) {
169+
g.Do("key", func() (i int, e error) {
108170
close(firstStarted)
109171
<-unblockFirst
110172
close(firstFinished)
@@ -115,15 +177,15 @@ func TestForget(t *testing.T) {
115177
g.Forget("key")
116178

117179
unblockSecond := make(chan struct{})
118-
secondResult := g.DoChan("key", func() (i any, e error) {
180+
secondResult := g.DoChan("key", func() (i int, e error) {
119181
<-unblockSecond
120182
return 2, nil
121183
})
122184

123185
close(unblockFirst)
124186
<-firstFinished
125187

126-
thirdResult := g.DoChan("key", func() (i any, e error) {
188+
thirdResult := g.DoChan("key", func() (i int, e error) {
127189
return 3, nil
128190
})
129191

@@ -223,11 +285,24 @@ func TestGoexitDo(t *testing.T) {
223285
}
224286
}
225287

226-
func TestPanicDoChan(t *testing.T) {
227-
if runtime.GOOS == "js" {
228-
t.Skipf("js does not support exec")
288+
func executable(t testing.TB) string {
289+
exe, err := os.Executable()
290+
if err != nil {
291+
t.Skipf("skipping: test executable not found")
292+
}
293+
294+
// Control case: check whether exec.Command works at all.
295+
// (For example, it might fail with a permission error on iOS.)
296+
cmd := exec.Command(exe, "-test.list=^$")
297+
cmd.Env = []string{}
298+
if err := cmd.Run(); err != nil {
299+
t.Skipf("skipping: exec appears not to work on %s: %v", runtime.GOOS, err)
229300
}
230301

302+
return exe
303+
}
304+
305+
func TestPanicDoChan(t *testing.T) {
231306
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
232307
defer func() {
233308
recover()
@@ -243,7 +318,7 @@ func TestPanicDoChan(t *testing.T) {
243318

244319
t.Parallel()
245320

246-
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
321+
cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
247322
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
248323
out := new(bytes.Buffer)
249324
cmd.Stdout = out
@@ -266,10 +341,6 @@ func TestPanicDoChan(t *testing.T) {
266341
}
267342

268343
func TestPanicDoSharedByDoChan(t *testing.T) {
269-
if runtime.GOOS == "js" {
270-
t.Skipf("js does not support exec")
271-
}
272-
273344
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
274345
blocked := make(chan struct{})
275346
unblock := make(chan struct{})
@@ -297,7 +368,7 @@ func TestPanicDoSharedByDoChan(t *testing.T) {
297368

298369
t.Parallel()
299370

300-
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
371+
cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
301372
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
302373
out := new(bytes.Buffer)
303374
cmd.Stdout = out
@@ -318,3 +389,33 @@ func TestPanicDoSharedByDoChan(t *testing.T) {
318389
t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
319390
}
320391
}
392+
393+
func ExampleGroup() {
394+
g := new(Group[string])
395+
396+
block := make(chan struct{})
397+
res1c := g.DoChan("key", func() (string, error) {
398+
<-block
399+
return "func 1", nil
400+
})
401+
res2c := g.DoChan("key", func() (string, error) {
402+
<-block
403+
return "func 2", nil
404+
})
405+
close(block)
406+
407+
res1 := <-res1c
408+
res2 := <-res2c
409+
410+
// Results are shared by functions executed with duplicate keys.
411+
fmt.Println("Shared:", res2.Shared)
412+
// Only the first function is executed: it is registered and started with "key",
413+
// and doesn't complete before the second function is registered with a duplicate key.
414+
fmt.Println("Equal results:", res1.Val == res2.Val)
415+
fmt.Println("Result:", res1.Val)
416+
417+
// Output:
418+
// Shared: true
419+
// Equal results: true
420+
// Result: func 1
421+
}

pkg/singleflight/singleflight.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
// Package singleflight provides a duplicate function call suppression
66
// mechanism.
7-
package singleflight
7+
package singleflight // import "golang.org/x/sync/singleflight"
88

99
import (
1010
"bytes"
@@ -31,6 +31,15 @@ func (p *panicError) Error() string {
3131
return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
3232
}
3333

34+
func (p *panicError) Unwrap() error {
35+
err, ok := p.value.(error)
36+
if !ok {
37+
return nil
38+
}
39+
40+
return err
41+
}
42+
3443
func newPanicError(v any) error {
3544
stack := debug.Stack()
3645

@@ -52,10 +61,6 @@ type call[T any] struct {
5261
val T
5362
err error
5463

55-
// forgotten indicates whether Forget was called with this call's key
56-
// while the call was still in flight.
57-
forgotten bool
58-
5964
// These fields are read and written with the singleflight
6065
// mutex held before the WaitGroup is done, and are read but
6166
// not written after the WaitGroup is done.
@@ -148,10 +153,10 @@ func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) {
148153
c.err = errGoexit
149154
}
150155

151-
c.wg.Done()
152156
g.mu.Lock()
153157
defer g.mu.Unlock()
154-
if !c.forgotten {
158+
c.wg.Done()
159+
if g.m[key] == c {
155160
delete(g.m, key)
156161
}
157162

@@ -204,9 +209,6 @@ func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) {
204209
// an earlier call to complete.
205210
func (g *Group[T]) Forget(key string) {
206211
g.mu.Lock()
207-
if c, ok := g.m[key]; ok {
208-
c.forgotten = true
209-
}
210212
delete(g.m, key)
211213
g.mu.Unlock()
212214
}

0 commit comments

Comments
 (0)