chore(pkg): update singleflight

This commit is contained in:
j2rong4cn
2025-07-05 13:31:20 +08:00
parent 92f396df10
commit 9612d61e60
2 changed files with 127 additions and 24 deletions

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package singleflight
package singleflight // import "golang.org/x/sync/singleflight"
import (
"bytes"
@ -19,6 +19,68 @@ import (
"time"
)
type errValue struct{}
func (err *errValue) Error() string {
return "error value"
}
func TestPanicErrorUnwrap(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
panicValue any
wrappedErrorType bool
}{
{
name: "panicError wraps non-error type",
panicValue: &panicError{value: "string value"},
wrappedErrorType: false,
},
{
name: "panicError wraps error type",
panicValue: &panicError{value: new(errValue)},
wrappedErrorType: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var recovered any
group := &Group[any]{}
func() {
defer func() {
recovered = recover()
t.Logf("after panic(%#v) in group.Do, recovered %#v", tc.panicValue, recovered)
}()
_, _, _ = group.Do(tc.name, func() (any, error) {
panic(tc.panicValue)
})
}()
if recovered == nil {
t.Fatal("expected a non-nil panic value")
}
err, ok := recovered.(error)
if !ok {
t.Fatalf("recovered non-error type: %T", recovered)
}
if !errors.Is(err, new(errValue)) && tc.wrappedErrorType {
t.Errorf("unexpected wrapped error type %T; want %T", err, new(errValue))
}
})
}
}
func TestDo(t *testing.T) {
var g Group[string]
v, err, _ := g.Do("key", func() (string, error) {
@ -95,7 +157,7 @@ func TestDoDupSuppress(t *testing.T) {
// Test that singleflight behaves correctly after Forget called.
// See https://github.com/golang/go/issues/31420
func TestForget(t *testing.T) {
var g Group[any]
var g Group[int]
var (
firstStarted = make(chan struct{})
@ -104,7 +166,7 @@ func TestForget(t *testing.T) {
)
go func() {
g.Do("key", func() (i any, e error) {
g.Do("key", func() (i int, e error) {
close(firstStarted)
<-unblockFirst
close(firstFinished)
@ -115,7 +177,7 @@ func TestForget(t *testing.T) {
g.Forget("key")
unblockSecond := make(chan struct{})
secondResult := g.DoChan("key", func() (i any, e error) {
secondResult := g.DoChan("key", func() (i int, e error) {
<-unblockSecond
return 2, nil
})
@ -123,7 +185,7 @@ func TestForget(t *testing.T) {
close(unblockFirst)
<-firstFinished
thirdResult := g.DoChan("key", func() (i any, e error) {
thirdResult := g.DoChan("key", func() (i int, e error) {
return 3, nil
})
@ -223,11 +285,24 @@ func TestGoexitDo(t *testing.T) {
}
}
func TestPanicDoChan(t *testing.T) {
if runtime.GOOS == "js" {
t.Skipf("js does not support exec")
func executable(t testing.TB) string {
exe, err := os.Executable()
if err != nil {
t.Skipf("skipping: test executable not found")
}
// Control case: check whether exec.Command works at all.
// (For example, it might fail with a permission error on iOS.)
cmd := exec.Command(exe, "-test.list=^$")
cmd.Env = []string{}
if err := cmd.Run(); err != nil {
t.Skipf("skipping: exec appears not to work on %s: %v", runtime.GOOS, err)
}
return exe
}
func TestPanicDoChan(t *testing.T) {
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
defer func() {
recover()
@ -243,7 +318,7 @@ func TestPanicDoChan(t *testing.T) {
t.Parallel()
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
out := new(bytes.Buffer)
cmd.Stdout = out
@ -266,10 +341,6 @@ func TestPanicDoChan(t *testing.T) {
}
func TestPanicDoSharedByDoChan(t *testing.T) {
if runtime.GOOS == "js" {
t.Skipf("js does not support exec")
}
if os.Getenv("TEST_PANIC_DOCHAN") != "" {
blocked := make(chan struct{})
unblock := make(chan struct{})
@ -297,7 +368,7 @@ func TestPanicDoSharedByDoChan(t *testing.T) {
t.Parallel()
cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
out := new(bytes.Buffer)
cmd.Stdout = out
@ -318,3 +389,33 @@ func TestPanicDoSharedByDoChan(t *testing.T) {
t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
}
}
func ExampleGroup() {
g := new(Group[string])
block := make(chan struct{})
res1c := g.DoChan("key", func() (string, error) {
<-block
return "func 1", nil
})
res2c := g.DoChan("key", func() (string, error) {
<-block
return "func 2", nil
})
close(block)
res1 := <-res1c
res2 := <-res2c
// Results are shared by functions executed with duplicate keys.
fmt.Println("Shared:", res2.Shared)
// Only the first function is executed: it is registered and started with "key",
// and doesn't complete before the second function is registered with a duplicate key.
fmt.Println("Equal results:", res1.Val == res2.Val)
fmt.Println("Result:", res1.Val)
// Output:
// Shared: true
// Equal results: true
// Result: func 1
}

View File

@ -4,7 +4,7 @@
// Package singleflight provides a duplicate function call suppression
// mechanism.
package singleflight
package singleflight // import "golang.org/x/sync/singleflight"
import (
"bytes"
@ -31,6 +31,15 @@ func (p *panicError) Error() string {
return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
}
func (p *panicError) Unwrap() error {
err, ok := p.value.(error)
if !ok {
return nil
}
return err
}
func newPanicError(v any) error {
stack := debug.Stack()
@ -52,10 +61,6 @@ type call[T any] struct {
val T
err error
// forgotten indicates whether Forget was called with this call's key
// while the call was still in flight.
forgotten bool
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// 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)) {
c.err = errGoexit
}
c.wg.Done()
g.mu.Lock()
defer g.mu.Unlock()
if !c.forgotten {
c.wg.Done()
if g.m[key] == c {
delete(g.m, key)
}
@ -204,9 +209,6 @@ func (g *Group[T]) doCall(c *call[T], key string, fn func() (T, error)) {
// an earlier call to complete.
func (g *Group[T]) Forget(key string) {
g.mu.Lock()
if c, ok := g.m[key]; ok {
c.forgotten = true
}
delete(g.m, key)
g.mu.Unlock()
}