Skip to content

Commit 96b31f7

Browse files
committed
Time cache implementation
Time cache implementation safe for concurrent use without the need of additional locking and coordination.
1 parent f22b8e7 commit 96b31f7

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

pkg/cache/cache.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Package cache provides a time cache implementation safe for concurrent use
2+
// without the need of additional locking.
3+
package cache
4+
5+
import (
6+
"container/list"
7+
"sync"
8+
"time"
9+
10+
"github.com/ipfs/go-log"
11+
)
12+
13+
var logger = log.Logger("keep-cache")
14+
15+
// TimeCache provides a time cache safe for concurrent use by
16+
// multiple goroutines without additional locking or coordination.
17+
type TimeCache struct {
18+
// all items in the cache in the order they were added
19+
// most recent items are on the front of the indexer;
20+
// it is used to optimize cache sweeping
21+
indexer *list.List
22+
// item in the cache with the timestamp it's been added
23+
// to the cache the last time
24+
cache map[string]time.Time
25+
// the timespan after which entry in the cache is considered
26+
// as outdated and can be removed from the cache
27+
timespan time.Duration
28+
mutex sync.RWMutex
29+
}
30+
31+
// NewTimeCache creates a new cache instance with provided timespan.
32+
func NewTimeCache(timespan time.Duration) *TimeCache {
33+
return &TimeCache{
34+
indexer: list.New(),
35+
cache: make(map[string]time.Time),
36+
timespan: timespan,
37+
}
38+
}
39+
40+
// Add adds an entry to the cache. Returns `true` if entry was not present in
41+
// the cache and was successfully added into it. Returns `false` if
42+
// entry is already in the cache. This method is synchronized.
43+
func (tc *TimeCache) Add(item string) bool {
44+
tc.mutex.Lock()
45+
defer tc.mutex.Unlock()
46+
47+
_, ok := tc.cache[item]
48+
if ok {
49+
return false
50+
}
51+
52+
// sweep old entries (those for which caching timespan has passed)
53+
for {
54+
back := tc.indexer.Back()
55+
if back == nil {
56+
break
57+
}
58+
59+
item := back.Value.(string)
60+
itemTime, ok := tc.cache[item]
61+
if !ok {
62+
logger.Errorf(
63+
"inconsistent cache state - expected item [%v] is not present",
64+
item,
65+
)
66+
break
67+
}
68+
69+
if time.Since(itemTime) > tc.timespan {
70+
tc.indexer.Remove(back)
71+
delete(tc.cache, item)
72+
} else {
73+
break
74+
}
75+
}
76+
77+
tc.cache[item] = time.Now()
78+
tc.indexer.PushFront(item)
79+
return true
80+
}
81+
82+
// Has checks presence of an entry in the cache. Returns `true` if entry is
83+
// present and `false` otherwise.
84+
func (tc *TimeCache) Has(item string) bool {
85+
tc.mutex.RLock()
86+
defer tc.mutex.RUnlock()
87+
88+
_, ok := tc.cache[item]
89+
return ok
90+
}

pkg/cache/cache_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cache
2+
3+
import (
4+
"strconv"
5+
"sync"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestAdd(t *testing.T) {
11+
cache := NewTimeCache(time.Minute)
12+
13+
cache.Add("test")
14+
15+
if !cache.Has("test") {
16+
t.Fatal("should have 'test' key")
17+
}
18+
}
19+
20+
func TestConcurrentAdd(t *testing.T) {
21+
cache := NewTimeCache(time.Minute)
22+
23+
var wg sync.WaitGroup
24+
wg.Add(10)
25+
26+
for i := 0; i < 10; i++ {
27+
go func(item int) {
28+
cache.Add(strconv.Itoa(item))
29+
wg.Done()
30+
}(i)
31+
}
32+
33+
wg.Wait()
34+
35+
for i := 0; i < 10; i++ {
36+
if !cache.Has(strconv.Itoa(i)) {
37+
t.Fatalf("should have '%v' key", i)
38+
}
39+
}
40+
}
41+
42+
func TestExpiration(t *testing.T) {
43+
cache := NewTimeCache(500 * time.Millisecond)
44+
for i := 0; i < 6; i++ {
45+
cache.Add(strconv.Itoa(i))
46+
time.Sleep(100 * time.Millisecond)
47+
}
48+
49+
if cache.Has(strconv.Itoa(0)) {
50+
t.Fatal("should have dropped '0' key from the cache already")
51+
}
52+
}

0 commit comments

Comments
 (0)