diff --git a/LICENCE b/LICENCE index 6a66aea..e202c9b 100644 --- a/LICENCE +++ b/LICENCE @@ -1,3 +1,5 @@ +Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. + Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e9a91c --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Singleflight with Generics! + +[![GoDoc](https://godoc.org/github.com/brunomvsouza/singleflight?status.svg)](https://godoc.org/github.com/brunomvsouza/singleflight) + +> Package singleflight provides a duplicate function call suppression mechanism. + +This fork adds generics support (`Group[K comparable, V any]` and `Result[V any]`) to the original [x/sync/singleflight](https://golang.org/x/sync/singleflight) package. + +### Updates + +- I will keep this package up-to-date with the original one, at least until `x/sync/singleflight` adds support for generics. If you notice an update before I do, please open an issue or submit a pull request. +- Versions will be tagged to align with the same versioning as the `x/sync/singleflight` package. + +### Usage + +For example usage, see: +- [Group.Do](examples/Do/main.go) +- [Group.DoChan](examples/DoChan/main.go) +- [Group.DoForget](examples/Forget/main.go) diff --git a/examples/Do/main.go b/examples/Do/main.go new file mode 100644 index 0000000..cde5161 --- /dev/null +++ b/examples/Do/main.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "sync" + "time" + + "github.com/brunomvsouza/singleflight" +) + +func main() { + var ( + group singleflight.Group[string, string] + wg sync.WaitGroup + ) + + wg.Add(2) + + go func() { + defer wg.Done() + res1, err1, shared1 := group.Do("key", func() (string, error) { + time.Sleep(10 * time.Millisecond) + return "func 1", nil + }) + fmt.Println("res1:", res1) + fmt.Println("err1:", err1) + fmt.Println("shared1:", shared1) + }() + + go func() { + defer wg.Done() + res2, err2, shared2 := group.Do("key", func() (string, error) { + time.Sleep(10 * time.Millisecond) + return "func 2", nil + }) + fmt.Println("res2:", res2) + fmt.Println("err2:", err2) + fmt.Println("shared2:", shared2) + }() + + wg.Wait() + + // Output: + // res1: func 2 + // err1: + // res2: func 2 + // err2: + // shared2: true + // shared1: true +} diff --git a/examples/DoChan/main.go b/examples/DoChan/main.go new file mode 100644 index 0000000..4b788a3 --- /dev/null +++ b/examples/DoChan/main.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + + "github.com/brunomvsouza/singleflight" +) + +func main() { + var group singleflight.Group[string, string] + + semaphore := make(chan struct{}) + + res1c := group.DoChan("key", func() (string, error) { + fmt.Printf("func 1 begin\n") + defer fmt.Printf("func 1 end\n") + <-semaphore + return "func 1", nil + }) + + res2c := group.DoChan("key", func() (string, error) { + fmt.Printf("func 2 begin\n") + defer fmt.Printf("func 2 end\n") + <-semaphore + return "func 2", nil + }) + + close(semaphore) + + 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 + // funtion 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 +} diff --git a/examples/Forget/main.go b/examples/Forget/main.go new file mode 100644 index 0000000..6c53b01 --- /dev/null +++ b/examples/Forget/main.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "sync" + "time" + + "github.com/brunomvsouza/singleflight" +) + +func main() { + var ( + group singleflight.Group[string, string] + wg sync.WaitGroup + ) + + wg.Add(4) + + go func() { + defer wg.Done() + res1, err1, shared1 := group.Do("key", func() (string, error) { + time.Sleep(1 * time.Second) + return "func 1", nil + }) + fmt.Println("res1:", res1) + fmt.Println("err1:", err1) + fmt.Println("shared1:", shared1) + }() + + go func() { + defer wg.Done() + res2, err2, shared2 := group.Do("key", func() (string, error) { + time.Sleep(1 * time.Second) + return "func 2", nil + }) + fmt.Println("res2:", res2) + fmt.Println("err2:", err2) + fmt.Println("shared2:", shared2) + }() + + go func() { + defer wg.Done() + time.Sleep(100 * time.Millisecond) + group.Forget("key") + }() + + go func() { + defer wg.Done() + time.Sleep(200 * time.Millisecond) + res3, err3, shared3 := group.Do("key", func() (string, error) { + time.Sleep(1 * time.Second) + return "func 3", nil + }) + fmt.Println("res3:", res3) + fmt.Println("err3:", err3) + fmt.Println("shared3:", shared3) + }() + + wg.Wait() + + // Output: + // res2: func 1 + // err2: + // shared2: true + // res1: func 1 + // err1: + // shared1: true + // res3: func 3 + // err3: + // shared3: false +} diff --git a/singleflight.go b/singleflight.go index 7635666..bcd1731 100644 --- a/singleflight.go +++ b/singleflight.go @@ -1,9 +1,17 @@ +// Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package singleflight provides a duplicate function call suppression // mechanism. +// +// This is a fork of Go's golang.org/x/sync/singleflight package, with the following +// changes: +// - Adds support for generics with Group[K comparable, V any] and Result[V any]. package singleflight import ( @@ -53,32 +61,32 @@ func newPanicError(v interface{}) error { } // call is an in-flight or completed singleflight.Do call -type call struct { +type call[V any] struct { wg sync.WaitGroup // These fields are written once before the WaitGroup is done // and are only read after the WaitGroup is done. - val interface{} + val V err error // 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. dups int - chans []chan<- Result + chans []chan<- Result[V] } // Group represents a class of work and forms a namespace in // which units of work can be executed with duplicate suppression. -type Group struct { - mu sync.Mutex // protects m - m map[string]*call // lazily initialized +type Group[K comparable, V any] struct { + mu sync.Mutex // protects m + m map[K]*call[V] // lazily initialized } // Result holds the results of Do, so they can be passed // on a channel. -type Result struct { - Val interface{} +type Result[V any] struct { + Val V Err error Shared bool } @@ -88,10 +96,10 @@ type Result struct { // time. If a duplicate comes in, the duplicate caller waits for the // original to complete and receives the same results. // The return value shared indicates whether v was given to multiple callers. -func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { +func (g *Group[K, V]) Do(key K, fn func() (V, error)) (v V, err error, shared bool) { g.mu.Lock() if g.m == nil { - g.m = make(map[string]*call) + g.m = make(map[K]*call[V]) } if c, ok := g.m[key]; ok { c.dups++ @@ -105,7 +113,7 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e } return c.val, c.err, true } - c := new(call) + c := new(call[V]) c.wg.Add(1) g.m[key] = c g.mu.Unlock() @@ -118,11 +126,11 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e // results when they are ready. // // The returned channel will not be closed. -func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result { - ch := make(chan Result, 1) +func (g *Group[K, V]) DoChan(key K, fn func() (V, error)) <-chan Result[V] { + ch := make(chan Result[V], 1) g.mu.Lock() if g.m == nil { - g.m = make(map[string]*call) + g.m = make(map[K]*call[V]) } if c, ok := g.m[key]; ok { c.dups++ @@ -130,7 +138,7 @@ func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result g.mu.Unlock() return ch } - c := &call{chans: []chan<- Result{ch}} + c := &call[V]{chans: []chan<- Result[V]{ch}} c.wg.Add(1) g.m[key] = c g.mu.Unlock() @@ -141,7 +149,7 @@ func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result } // doCall handles the single call for a key. -func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { +func (g *Group[K, V]) doCall(c *call[V], key K, fn func() (V, error)) { normalReturn := false recovered := false @@ -174,7 +182,7 @@ func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { } else { // Normal return for _, ch := range c.chans { - ch <- Result{c.val, c.err, c.dups > 0} + ch <- Result[V]{c.val, c.err, c.dups > 0} } } }() @@ -207,7 +215,7 @@ func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { // Forget tells the singleflight to forget about a key. Future calls // to Do for this key will call the function rather than waiting for // an earlier call to complete. -func (g *Group) Forget(key string) { +func (g *Group[K, V]) Forget(key K) { g.mu.Lock() delete(g.m, key) g.mu.Unlock() diff --git a/singleflight_test.go b/singleflight_test.go index 0fc8936..5cbdf88 100644 --- a/singleflight_test.go +++ b/singleflight_test.go @@ -1,3 +1,7 @@ +// Copyright (c) 2023 Bruno Marques Venceslau de Souza. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -53,7 +57,7 @@ func TestPanicErrorUnwrap(t *testing.T) { var recovered interface{} - group := &Group{} + group := &Group[string, any]{} func() { defer func() { @@ -83,7 +87,7 @@ func TestPanicErrorUnwrap(t *testing.T) { } func TestDo(t *testing.T) { - var g Group + var g Group[string, any] v, err, _ := g.Do("key", func() (interface{}, error) { return "bar", nil }) @@ -96,7 +100,7 @@ func TestDo(t *testing.T) { } func TestDoErr(t *testing.T) { - var g Group + var g Group[string, any] someErr := errors.New("Some error") v, err, _ := g.Do("key", func() (interface{}, error) { return nil, someErr @@ -110,7 +114,7 @@ func TestDoErr(t *testing.T) { } func TestDoDupSuppress(t *testing.T) { - var g Group + var g Group[string, any] var wg1, wg2 sync.WaitGroup c := make(chan string, 1) var calls int32 @@ -158,7 +162,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 + var g Group[string, any] var ( firstStarted = make(chan struct{}) @@ -199,7 +203,7 @@ func TestForget(t *testing.T) { } func TestDoChan(t *testing.T) { - var g Group + var g Group[string, any] ch := g.DoChan("key", func() (interface{}, error) { return "bar", nil }) @@ -218,7 +222,7 @@ func TestDoChan(t *testing.T) { // Test singleflight behaves correctly after Do panic. // See https://github.com/golang/go/issues/41133 func TestPanicDo(t *testing.T) { - var g Group + var g Group[string, any] fn := func() (interface{}, error) { panic("invalid memory address or nil pointer dereference") } @@ -255,7 +259,7 @@ func TestPanicDo(t *testing.T) { } func TestGoexitDo(t *testing.T) { - var g Group + var g Group[string, any] fn := func() (interface{}, error) { runtime.Goexit() return nil, nil @@ -309,7 +313,7 @@ func TestPanicDoChan(t *testing.T) { _ = recover() }() - g := new(Group) + g := new(Group[string, any]) ch := g.DoChan("", func() (interface{}, error) { panic("Panicking in DoChan") }) @@ -346,7 +350,7 @@ func TestPanicDoSharedByDoChan(t *testing.T) { blocked := make(chan struct{}) unblock := make(chan struct{}) - g := new(Group) + g := new(Group[string, any]) go func() { defer func() { _ = recover() @@ -392,14 +396,14 @@ func TestPanicDoSharedByDoChan(t *testing.T) { } func ExampleGroup() { - g := new(Group) + g := new(Group[string, string]) block := make(chan struct{}) - res1c := g.DoChan("key", func() (interface{}, error) { + res1c := g.DoChan("key", func() (string, error) { <-block return "func 1", nil }) - res2c := g.DoChan("key", func() (interface{}, error) { + res2c := g.DoChan("key", func() (string, error) { <-block return "func 2", nil }) @@ -412,7 +416,7 @@ func ExampleGroup() { 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 funtion is registered with a duplicate key. - fmt.Println("Equal results:", res1.Val.(string) == res2.Val.(string)) + fmt.Println("Equal results:", res1.Val == res2.Val) fmt.Println("Result:", res1.Val) // Output: