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 // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package singleflight package singleflight // import "golang.org/x/sync/singleflight"
import ( import (
"bytes" "bytes"
@ -19,6 +19,68 @@ import (
"time" "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) { func TestDo(t *testing.T) {
var g Group[string] var g Group[string]
v, err, _ := g.Do("key", func() (string, error) { 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. // Test that singleflight behaves correctly after Forget called.
// See https://github.com/golang/go/issues/31420 // See https://github.com/golang/go/issues/31420
func TestForget(t *testing.T) { func TestForget(t *testing.T) {
var g Group[any] var g Group[int]
var ( var (
firstStarted = make(chan struct{}) firstStarted = make(chan struct{})
@ -104,7 +166,7 @@ func TestForget(t *testing.T) {
) )
go func() { go func() {
g.Do("key", func() (i any, e error) { g.Do("key", func() (i int, e error) {
close(firstStarted) close(firstStarted)
<-unblockFirst <-unblockFirst
close(firstFinished) close(firstFinished)
@ -115,7 +177,7 @@ func TestForget(t *testing.T) {
g.Forget("key") g.Forget("key")
unblockSecond := make(chan struct{}) unblockSecond := make(chan struct{})
secondResult := g.DoChan("key", func() (i any, e error) { secondResult := g.DoChan("key", func() (i int, e error) {
<-unblockSecond <-unblockSecond
return 2, nil return 2, nil
}) })
@ -123,7 +185,7 @@ func TestForget(t *testing.T) {
close(unblockFirst) close(unblockFirst)
<-firstFinished <-firstFinished
thirdResult := g.DoChan("key", func() (i any, e error) { thirdResult := g.DoChan("key", func() (i int, e error) {
return 3, nil return 3, nil
}) })
@ -223,11 +285,24 @@ func TestGoexitDo(t *testing.T) {
} }
} }
func TestPanicDoChan(t *testing.T) { func executable(t testing.TB) string {
if runtime.GOOS == "js" { exe, err := os.Executable()
t.Skipf("js does not support exec") 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") != "" { if os.Getenv("TEST_PANIC_DOCHAN") != "" {
defer func() { defer func() {
recover() recover()
@ -243,7 +318,7 @@ func TestPanicDoChan(t *testing.T) {
t.Parallel() 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") cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
out := new(bytes.Buffer) out := new(bytes.Buffer)
cmd.Stdout = out cmd.Stdout = out
@ -266,10 +341,6 @@ func TestPanicDoChan(t *testing.T) {
} }
func TestPanicDoSharedByDoChan(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") != "" { if os.Getenv("TEST_PANIC_DOCHAN") != "" {
blocked := make(chan struct{}) blocked := make(chan struct{})
unblock := make(chan struct{}) unblock := make(chan struct{})
@ -297,7 +368,7 @@ func TestPanicDoSharedByDoChan(t *testing.T) {
t.Parallel() 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") cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
out := new(bytes.Buffer) out := new(bytes.Buffer)
cmd.Stdout = out 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") 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 // Package singleflight provides a duplicate function call suppression
// mechanism. // mechanism.
package singleflight package singleflight // import "golang.org/x/sync/singleflight"
import ( import (
"bytes" "bytes"
@ -31,6 +31,15 @@ func (p *panicError) Error() string {
return fmt.Sprintf("%v\n\n%s", p.value, p.stack) 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 { func newPanicError(v any) error {
stack := debug.Stack() stack := debug.Stack()
@ -52,10 +61,6 @@ type call[T any] struct {
val T val T
err error 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 // These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but // mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done. // 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.err = errGoexit
} }
c.wg.Done()
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
if !c.forgotten { c.wg.Done()
if g.m[key] == c {
delete(g.m, key) 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. // an earlier call to complete.
func (g *Group[T]) Forget(key string) { func (g *Group[T]) Forget(key string) {
g.mu.Lock() g.mu.Lock()
if c, ok := g.m[key]; ok {
c.forgotten = true
}
delete(g.m, key) delete(g.m, key)
g.mu.Unlock() g.mu.Unlock()
} }