Skip to content

Commit 2baeae8

Browse files
committed
Use method is introduced(with tests)
1 parent 37dd593 commit 2baeae8

File tree

2 files changed

+157
-3
lines changed

2 files changed

+157
-3
lines changed

singleflight/singleflight.go

+39-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
// mechanism.
77
package singleflight // import "golang.org/x/sync/singleflight"
88

9-
import "sync"
9+
import (
10+
"sync"
11+
"sync/atomic"
12+
)
1013

1114
// call is an in-flight or completed singleflight.Do call
1215
type call struct {
@@ -49,6 +52,39 @@ type Result struct {
4952
// original to complete and receives the same results.
5053
// The return value shared indicates whether v was given to multiple callers.
5154
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
55+
c := g.doNoChan(key, fn)
56+
return c.val, c.err, c.dups > 0
57+
}
58+
59+
// 'Use' calls 'new' at most once at a time, then invokes 'fn' with the resulting values.
60+
// The 'dispose' argument invokes after the last call to fn has returned.
61+
//
62+
// `Use` is designed for scenario when 'new' generates a temporary resource, which has to be cleaned up after last 'fn' is done using it
63+
// Notes:
64+
// 'dispose' is called at most once, after last fn been completed. 'dispose' will NOT get called if/when 'new' returns an error
65+
// 'fn' is called on each goroutine with values returned by 'new', regardless of whether or not 'new' returned an error
66+
// results of 'new' are passed to 'fn'.
67+
//
68+
// Return: 'Use' propagates return value from 'fn'
69+
func (g *Group) Use(
70+
key string,
71+
new func() (interface{}, error),
72+
fn func(interface{}, error) error,
73+
dispose func(interface{}),
74+
) error {
75+
c := g.doNoChan(key, new)
76+
if c.err == nil && dispose != nil {
77+
defer func() {
78+
if atomic.AddInt64(&c.dups, -1) == -1 {
79+
dispose(c.val)
80+
}
81+
}()
82+
}
83+
84+
return fn(c.val, c.err)
85+
}
86+
87+
func (g *Group) doNoChan(key string, fn func() (interface{}, error)) *call {
5288
g.mu.Lock()
5389
if g.m == nil {
5490
g.m = make(map[string]*call)
@@ -57,15 +93,15 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e
5793
c.dups++
5894
g.mu.Unlock()
5995
c.wg.Wait()
60-
return c.val, c.err, true
96+
return c
6197
}
6298
c := new(call)
6399
c.wg.Add(1)
64100
g.m[key] = c
65101
g.mu.Unlock()
66102

67103
g.doCall(c, key, fn)
68-
return c.val, c.err, c.dups > 0
104+
return c
69105
}
70106

71107
// DoChan is like Do but returns a channel that will receive the

singleflight/singleflight_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,130 @@ package singleflight
77
import (
88
"errors"
99
"fmt"
10+
"io/ioutil"
11+
"os"
1012
"sync"
1113
"sync/atomic"
1214
"testing"
1315
"time"
1416
)
1517

18+
func testConcurrentHelper(t *testing.T, inGoroutine func(routineIndex, goroutineCount int)) {
19+
var wg, wgGoroutines sync.WaitGroup
20+
const callers = 4
21+
//ref := make([]RefCounter, callers)
22+
wgGoroutines.Add(callers)
23+
for i := 0; i < callers; i++ {
24+
wg.Add(1)
25+
go func(index int) {
26+
defer wg.Done()
27+
28+
wgGoroutines.Done()
29+
wgGoroutines.Wait() // ensure that all goroutines started and reached this point
30+
31+
inGoroutine(i, callers)
32+
}(i)
33+
}
34+
wg.Wait()
35+
36+
}
37+
38+
func TestUse(t *testing.T) {
39+
var g Group
40+
var newCount, handleCount, disposeCount int64
41+
42+
testConcurrentHelper(
43+
t,
44+
func(index, goroutineCount int) {
45+
g.Use(
46+
"key",
47+
// 'new' is a slow function that creates a temp resource
48+
func() (interface{}, error) {
49+
time.Sleep(200 * time.Millisecond) // let more goroutines enter Do
50+
atomic.AddInt64(&newCount, 1)
51+
return "bar", nil
52+
},
53+
// 'fn' to be called by each goroutine
54+
func(s interface{}, e error) error {
55+
// handle s
56+
if newCount != 1 {
57+
t.Errorf("goroutine %v: newCount(%v) expected to be set prior to this function getting called", index, newCount)
58+
}
59+
atomic.AddInt64(&handleCount, 1)
60+
if disposeCount > 0 {
61+
t.Errorf("goroutine %v: disposeCount(%v) should not be incremented until all fn are completed", index, disposeCount)
62+
}
63+
return e
64+
},
65+
// 'dispose' - to be called once at the end
66+
func(s interface{}) {
67+
// cleaning up "bar"
68+
atomic.AddInt64(&disposeCount, 1)
69+
if handleCount != int64(goroutineCount) {
70+
t.Errorf("dispose is expected to be called when all %v fn been completed, but %v have been completed instead", goroutineCount, handleCount)
71+
}
72+
},
73+
)
74+
},
75+
)
76+
77+
if newCount != 1 {
78+
t.Errorf("new expected to be called exactly once, was called %v", newCount)
79+
}
80+
if disposeCount != 1 {
81+
t.Errorf("dispose expected to be called exactly once, was called %v", disposeCount)
82+
}
83+
}
84+
85+
func TestUseWithResource(t *testing.T) {
86+
// use this "global" var for checkes after that testConcurrentHelper call
87+
var tempFileName string
88+
89+
var g Group
90+
testConcurrentHelper(
91+
t,
92+
func(_, _ int) {
93+
g.Use(
94+
"key",
95+
// 'new' is a slow function that creates a temp resource
96+
func() (interface{}, error) {
97+
time.Sleep(200 * time.Millisecond) // let more goroutines enter Do
98+
f, e := ioutil.TempFile("", "pat")
99+
if e != nil {
100+
return nil, e
101+
}
102+
defer f.Close()
103+
tempFileName = f.Name()
104+
105+
// fill temp file with sequence of n.Write(...) calls
106+
107+
return f.Name(), e
108+
},
109+
// 'fn' to be called by each goroutine
110+
func(s interface{}, e error) error {
111+
// handle s
112+
if e != nil {
113+
// send alternative payload
114+
}
115+
if e == nil {
116+
/*tempFileName*/ _ = s.(string)
117+
// send Content of tempFileName to HTTPWriter
118+
}
119+
return e
120+
},
121+
// 'dispose' - to be called once at the end
122+
func(s interface{}) {
123+
// cleaning up "bar"
124+
os.Remove(s.(string))
125+
},
126+
)
127+
},
128+
)
129+
if _, e := os.Stat(tempFileName); !os.IsNotExist(e) {
130+
t.Errorf("test has created a temp file '%v', but failed to cleaned it", tempFileName)
131+
}
132+
}
133+
16134
func TestDo(t *testing.T) {
17135
var g Group
18136
v, err, _ := g.Do("key", func() (interface{}, error) {

0 commit comments

Comments
 (0)