Skip to content

Commit bd36cd2

Browse files
authored
Merge pull request #121 from keep-network/generic-time-cache
Generic time cache
2 parents f893494 + a8a6659 commit bd36cd2

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

pkg/cache/generic_cache.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
11+
type genericCacheEntry[T any] struct {
12+
value T
13+
timestamp time.Time
14+
}
15+
16+
// GenericTimeCache provides a generic time cache safe for concurrent use by
17+
// multiple goroutines without additional locking or coordination.
18+
// The implementation is based on the simple TimeCache.
19+
type GenericTimeCache[T any] struct {
20+
// all keys in the cache in the order they were added
21+
// most recent keys are on the front of the indexer;
22+
// it is used to optimize cache sweeping
23+
indexer *list.List
24+
// key in the cache with the value and timestamp it's been added
25+
// to the cache the last time
26+
cache map[string]*genericCacheEntry[T]
27+
// the timespan after which entry in the cache is considered
28+
// as outdated and can be removed from the cache
29+
timespan time.Duration
30+
mutex sync.RWMutex
31+
}
32+
33+
// NewGenericTimeCache creates a new generic cache instance with provided timespan.
34+
func NewGenericTimeCache[T any](timespan time.Duration) *GenericTimeCache[T] {
35+
return &GenericTimeCache[T]{
36+
indexer: list.New(),
37+
cache: make(map[string]*genericCacheEntry[T]),
38+
timespan: timespan,
39+
}
40+
}
41+
42+
// Add adds an entry to the cache. Returns `true` if entry was not present in
43+
// the cache and was successfully added into it. Returns `false` if
44+
// entry is already in the cache. This method is synchronized.
45+
func (tc *GenericTimeCache[T]) Add(key string, value T) bool {
46+
tc.mutex.Lock()
47+
defer tc.mutex.Unlock()
48+
49+
_, ok := tc.cache[key]
50+
if ok {
51+
return false
52+
}
53+
54+
tc.sweep()
55+
56+
tc.cache[key] = &genericCacheEntry[T]{
57+
value: value,
58+
timestamp: time.Now(),
59+
}
60+
tc.indexer.PushFront(key)
61+
return true
62+
}
63+
64+
// Get gets an entry from the cache. Boolean flag is `true` if entry is
65+
// present and `false` otherwise.
66+
func (tc *GenericTimeCache[T]) Get(key string) (T, bool) {
67+
tc.mutex.RLock()
68+
defer tc.mutex.RUnlock()
69+
70+
entry, ok := tc.cache[key]
71+
if !ok {
72+
var zeroValue T
73+
return zeroValue, ok
74+
}
75+
76+
return entry.value, ok
77+
}
78+
79+
// Sweep removes old entries. That is those for which caching timespan has
80+
// passed.
81+
func (tc *GenericTimeCache[T]) Sweep() {
82+
tc.mutex.Lock()
83+
defer tc.mutex.Unlock()
84+
85+
tc.sweep()
86+
}
87+
88+
func (tc *GenericTimeCache[T]) sweep() {
89+
for {
90+
back := tc.indexer.Back()
91+
if back == nil {
92+
break
93+
}
94+
95+
key := back.Value.(string)
96+
entry, ok := tc.cache[key]
97+
if !ok {
98+
logger.Errorf(
99+
"inconsistent cache state - expected key [%v] is not present",
100+
key,
101+
)
102+
break
103+
}
104+
105+
if time.Since(entry.timestamp) > tc.timespan {
106+
tc.indexer.Remove(back)
107+
delete(tc.cache, key)
108+
} else {
109+
break
110+
}
111+
}
112+
}

pkg/cache/generic_cache_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cache
2+
3+
import (
4+
"reflect"
5+
"strconv"
6+
"sync"
7+
"testing"
8+
"time"
9+
)
10+
11+
type valueType struct {
12+
field int
13+
}
14+
15+
func TestGenericTimeCache_Add(t *testing.T) {
16+
cache := NewGenericTimeCache[*valueType](time.Minute)
17+
18+
cache.Add("test", &valueType{10})
19+
20+
value, ok := cache.Get("test")
21+
if !ok {
22+
t.Fatal("should have 'test' key")
23+
}
24+
25+
expectedValue := &valueType{10}
26+
if !reflect.DeepEqual(expectedValue, value) {
27+
t.Errorf(
28+
"unexpected value: \n"+
29+
"exptected: %v\n"+
30+
"actual: %v",
31+
expectedValue,
32+
value,
33+
)
34+
}
35+
}
36+
37+
func TestGenericTimeCache_ConcurrentAdd(t *testing.T) {
38+
cache := NewGenericTimeCache[*valueType](time.Minute)
39+
40+
var wg sync.WaitGroup
41+
wg.Add(10)
42+
43+
for i := 0; i < 10; i++ {
44+
go func(item int) {
45+
cache.Add(strconv.Itoa(item), &valueType{item})
46+
wg.Done()
47+
}(i)
48+
}
49+
50+
wg.Wait()
51+
52+
for i := 0; i < 10; i++ {
53+
value, ok := cache.Get(strconv.Itoa(i))
54+
if !ok {
55+
t.Fatalf("should have '%v' key", i)
56+
}
57+
58+
expectedValue := &valueType{i}
59+
if !reflect.DeepEqual(expectedValue, value) {
60+
t.Errorf(
61+
"unexpected value: \n"+
62+
"exptected: %v\n"+
63+
"actual: %v",
64+
expectedValue,
65+
value,
66+
)
67+
}
68+
}
69+
}
70+
71+
func TestGenericTimeCache_Expiration(t *testing.T) {
72+
cache := NewGenericTimeCache[*valueType](500 * time.Millisecond)
73+
for i := 0; i < 6; i++ {
74+
cache.Add(strconv.Itoa(i), &valueType{i})
75+
time.Sleep(100 * time.Millisecond)
76+
}
77+
78+
if _, ok := cache.Get(strconv.Itoa(0)); ok {
79+
t.Fatal("should have dropped '0' key from the cache")
80+
}
81+
}
82+
83+
func TestGenericTimeCache_Sweep(t *testing.T) {
84+
cache := NewGenericTimeCache[*valueType](500 * time.Millisecond)
85+
cache.Add("old", &valueType{10})
86+
time.Sleep(100 * time.Millisecond)
87+
cache.Add("new", &valueType{20})
88+
time.Sleep(400 * time.Millisecond)
89+
90+
cache.Sweep()
91+
92+
if _, ok := cache.Get("old"); ok {
93+
t.Fatal("should have dropped 'old' key from the cache")
94+
}
95+
if _, ok := cache.Get("new"); !ok {
96+
t.Fatal("should still have 'new' in the cache")
97+
}
98+
}

0 commit comments

Comments
 (0)