Skip to content

Commit edfd85c

Browse files
committed
feat: add singleflight wrapper for fastcache stores
1 parent cb1ad8a commit edfd85c

File tree

4 files changed

+117
-1
lines changed

4 files changed

+117
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
go.work.sum

fastcache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ var cacheNoStore = []byte("no-store")
9696
// New creates and returns a new FastCache instance.
9797
func New(s Store) *FastCache {
9898
return &FastCache{
99-
s: s,
99+
s: newSingleflightStore(s),
100100
}
101101
}
102102

singleflight.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package fastcache
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"golang.org/x/sync/singleflight"
8+
)
9+
10+
// singleFlightStore wraps a Store with singleflight functionality
11+
type singleFlightStore struct {
12+
store Store
13+
sf singleflight.Group
14+
}
15+
16+
// newSingleflightStore creates a new SingleflightStore
17+
func newSingleflightStore(store Store) *singleFlightStore {
18+
return &singleFlightStore{
19+
store: store,
20+
}
21+
}
22+
23+
// Get retrieves an item from the store using singleflight
24+
func (s *singleFlightStore) Get(namespace, group, uri string) (Item, error) {
25+
key := fmt.Sprintf("%s:%s:%s", namespace, group, uri)
26+
27+
v, err, _ := s.sf.Do(key, func() (interface{}, error) {
28+
return s.store.Get(namespace, group, uri)
29+
})
30+
31+
if err != nil {
32+
return Item{}, err
33+
}
34+
35+
// Handle the case where the item doesn't exist
36+
if v == nil {
37+
return Item{}, nil
38+
}
39+
40+
// Check the type of v is Item
41+
item, ok := v.(Item)
42+
if !ok {
43+
return Item{}, fmt.Errorf("unexpected type %T", v)
44+
}
45+
46+
return item, nil
47+
}
48+
49+
// Put adds an item to the underlying store
50+
func (s *singleFlightStore) Put(namespace, group, uri string, b Item, ttl time.Duration) error {
51+
return s.store.Put(namespace, group, uri, b, ttl)
52+
}
53+
54+
// Del removes an item from the underlying store
55+
func (s *singleFlightStore) Del(namespace, group, uri string) error {
56+
return s.store.Del(namespace, group, uri)
57+
}
58+
59+
// DelGroup removes a group of items from the underlying store
60+
func (s *singleFlightStore) DelGroup(namespace string, group ...string) error {
61+
return s.store.DelGroup(namespace, group...)
62+
}

singleflight_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package fastcache
2+
3+
import (
4+
"sync"
5+
"testing"
6+
"time"
7+
)
8+
9+
type testSlowStore struct {
10+
getCount int
11+
delay time.Duration
12+
}
13+
14+
func (s *testSlowStore) Get(namespace, group, uri string) (Item, error) {
15+
time.Sleep(s.delay)
16+
s.getCount++
17+
return Item{}, nil
18+
}
19+
20+
func (s *testSlowStore) Put(namespace, group, uri string, b Item, ttl time.Duration) error {
21+
return nil
22+
}
23+
24+
func (s *testSlowStore) Del(namespace, group, uri string) error {
25+
return nil
26+
}
27+
28+
func (s *testSlowStore) DelGroup(namespace string, group ...string) error {
29+
return nil
30+
}
31+
32+
func TestSingleFlightStore(t *testing.T) {
33+
slowStore := &testSlowStore{delay: 100 * time.Millisecond}
34+
sfs := newSingleflightStore(slowStore)
35+
36+
sfs.Put("namespace", "group", "uri", Item{}, 0)
37+
38+
// Call Get 10 times concurrently
39+
var wg sync.WaitGroup
40+
wg.Add(10)
41+
for i := 0; i < 10; i++ {
42+
go func() {
43+
_, _ = sfs.Get("namespace", "group", "uri")
44+
wg.Done()
45+
}()
46+
}
47+
48+
wg.Wait()
49+
50+
if slowStore.getCount != 1 {
51+
t.Errorf("expected 1, got %d", slowStore.getCount)
52+
}
53+
}

0 commit comments

Comments
 (0)