Skip to content

Commit b222d1b

Browse files
committed
Add unit tests
1 parent 52e511c commit b222d1b

File tree

6 files changed

+172
-13
lines changed

6 files changed

+172
-13
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/abihf/cache-loader
22

33
go 1.14
44

5-
require github.com/hashicorp/golang-lru v0.5.4
5+
require (
6+
github.com/hashicorp/golang-lru v0.5.4
7+
github.com/stretchr/testify v1.5.1
8+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
24
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8+
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
9+
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
12+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

inmemory_cache.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package loader
2+
3+
import "sync"
4+
5+
type inMemoryCache struct {
6+
sync.Map
7+
}
8+
9+
func InMemoryCache() Cache {
10+
return &inMemoryCache{sync.Map{}}
11+
}
12+
13+
func (c *inMemoryCache) Add(key, value interface{}) {
14+
c.Map.Store(key, value)
15+
}
16+
17+
func (c *inMemoryCache) Get(key interface{}) (value interface{}, ok bool) {
18+
return c.Map.Load(key)
19+
}
20+
21+
func (c *inMemoryCache) Remove(key interface{}) {
22+
c.Map.Delete(key)
23+
}

loader.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package loader
22

33
import (
44
"sync"
5+
"sync/atomic"
56
"time"
67
)
78

@@ -49,19 +50,17 @@ func (l *Loader) Get(key interface{}) (interface{}, error) {
4950
item.mutex.Lock()
5051
defer item.mutex.Unlock()
5152

52-
if item.expire.Before(time.Now()) && !item.isFetching {
53-
item.isFetching = true // so other thread don't fetch
53+
// if the item is expired and it's not doing refetch
54+
if item.expire.Before(time.Now()) && atomic.CompareAndSwapInt32(&item.isFetching, 0, 1) {
5455
go l.refetch(key, item)
5556
}
5657
return item.value, nil
5758
}
5859

59-
item := &cacheItem{isFetching: true, mutex: sync.Mutex{}}
60+
item := &cacheItem{isFetching: 0, mutex: sync.Mutex{}}
6061
item.mutex.Lock()
6162
defer item.mutex.Unlock()
62-
defer func() {
63-
item.isFetching = false
64-
}()
63+
6564
l.cache.Add(key, item)
6665
l.mutex.Unlock()
6766

@@ -76,16 +75,17 @@ func (l *Loader) Get(key interface{}) (interface{}, error) {
7675
}
7776

7877
func (l *Loader) refetch(key interface{}, item *cacheItem) {
79-
item.isFetching = true // to make sure, lol
80-
defer func() {
81-
item.isFetching = false
82-
}()
78+
defer atomic.StoreInt32(&item.isFetching, 0)
8379

8480
value, err := l.fn(key)
8581
if err != nil {
8682
l.cache.Remove(key)
8783
return
8884
}
85+
86+
item.mutex.Lock()
87+
defer item.mutex.Unlock()
88+
8989
item.value = value
9090
item.updateExpire(l.ttl)
9191
}
@@ -95,7 +95,7 @@ type cacheItem struct {
9595
expire time.Time
9696

9797
mutex sync.Mutex
98-
isFetching bool
98+
isFetching int32
9999
}
100100

101101
func (i *cacheItem) updateExpire(ttl time.Duration) {

loader_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package loader
2+
3+
import (
4+
"fmt"
5+
"sync/atomic"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestConcurrencySingleKey(t *testing.T) {
13+
var counter int32
14+
fetch := func(key interface{}) (interface{}, error) {
15+
atomic.AddInt32(&counter, 1)
16+
time.Sleep(100 * time.Millisecond)
17+
return key, nil
18+
}
19+
l := New(fetch, 500*time.Millisecond, InMemoryCache())
20+
type result struct {
21+
dur time.Duration
22+
val interface{}
23+
}
24+
c := make(chan *result, 3)
25+
26+
var start time.Time
27+
var dur time.Duration
28+
29+
start = time.Now()
30+
for i := 0; i < 3; i++ {
31+
go func() {
32+
start := time.Now()
33+
val, _ := l.Get("x")
34+
c <- &result{val: val, dur: time.Now().Sub(start)}
35+
}()
36+
time.Sleep(10 * time.Millisecond)
37+
}
38+
for i := 0; i < 3; i++ {
39+
res := <-c
40+
assert.InDelta(t, 100, res.dur.Milliseconds(), 25, "each get should within 1s")
41+
assert.Equal(t, "x", res.val, "Value must be x")
42+
}
43+
dur = time.Now().Sub(start)
44+
assert.InDelta(t, 100, dur.Milliseconds(), 25, "all get should within 1s")
45+
46+
start = time.Now()
47+
val, _ := l.Get("x")
48+
dur = time.Now().Sub(start)
49+
assert.Less(t, dur.Milliseconds(), int64(50), "After cached get must be fast")
50+
assert.Equal(t, "x", val, "Value must still be x")
51+
52+
assert.Equal(t, int32(1), counter, "fetch must be called once")
53+
}
54+
55+
func TestConcurrencyMultiKey(t *testing.T) {
56+
var counter int32
57+
fetch := func(key interface{}) (interface{}, error) {
58+
atomic.AddInt32(&counter, 1)
59+
time.Sleep(100 * time.Millisecond)
60+
return key, nil
61+
}
62+
l := New(fetch, 500*time.Millisecond, InMemoryCache())
63+
type result struct {
64+
dur time.Duration
65+
val interface{}
66+
}
67+
c := make(chan *result, 3)
68+
69+
var start time.Time
70+
var dur time.Duration
71+
72+
start = time.Now()
73+
for i := 0; i < 3; i++ {
74+
go func(i int) {
75+
start := time.Now()
76+
val, _ := l.Get(fmt.Sprint(i))
77+
c <- &result{val: val, dur: time.Now().Sub(start)}
78+
}(i)
79+
time.Sleep(10 * time.Millisecond)
80+
}
81+
for i := 0; i < 3; i++ {
82+
res := <-c
83+
assert.InDelta(t, 100, res.dur.Milliseconds(), 25, "each get should within 1s")
84+
assert.Equal(t, fmt.Sprint(i), res.val, "Value must be valid")
85+
}
86+
dur = time.Now().Sub(start)
87+
assert.InDelta(t, 100, dur.Milliseconds(), 25, "all get should within 1s")
88+
89+
start = time.Now()
90+
val, _ := l.Get("1")
91+
dur = time.Now().Sub(start)
92+
assert.Less(t, dur.Milliseconds(), int64(50), "After cached get must be fast")
93+
assert.Equal(t, "1", val, "Value must still the same")
94+
95+
assert.Equal(t, int32(3), counter, "fetch must be called once")
96+
}
97+
98+
func TestExpire(t *testing.T) {
99+
var counter int32
100+
fetch := func(key interface{}) (interface{}, error) {
101+
atomic.AddInt32(&counter, 1)
102+
time.Sleep(10 * time.Millisecond)
103+
return fmt.Sprintf("%d %s", counter, key), nil
104+
}
105+
l := New(fetch, 500*time.Millisecond, InMemoryCache())
106+
val, _ := l.Get("x")
107+
assert.Equal(t, "1 x", val, "First call")
108+
assert.Equal(t, int32(1), counter, "fetch called once")
109+
110+
time.Sleep(550 * time.Millisecond)
111+
val, _ = l.Get("x")
112+
assert.Equal(t, "1 x", val, "Use stale value")
113+
val, _ = l.Get("x")
114+
assert.Equal(t, "1 x", val, "Still use stale value")
115+
116+
time.Sleep(100 * time.Millisecond)
117+
val, _ = l.Get("x")
118+
assert.Equal(t, "2 x", val, "Use updated value")
119+
assert.Equal(t, int32(2), counter, "fetch called twice")
120+
}

lru.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func (c *LRUCache) Remove(key interface{}) {
2323

2424
// NewLRU creates Loader with lru based cache
2525
func NewLRU(fn LoadFunc, ttl time.Duration, size int) *Loader {
26-
cache, _ := lru.New(size)
26+
cache, err := lru.New(size)
27+
if err != nil {
28+
panic(err)
29+
}
2730
return New(fn, ttl, &LRUCache{cache})
2831
}

0 commit comments

Comments
 (0)