-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #121 from keep-network/generic-time-cache
Generic time cache
- Loading branch information
Showing
2 changed files
with
210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// Package cache provides a time cache implementation safe for concurrent use | ||
// without the need of additional locking. | ||
package cache | ||
|
||
import ( | ||
"container/list" | ||
"sync" | ||
"time" | ||
) | ||
|
||
type genericCacheEntry[T any] struct { | ||
value T | ||
timestamp time.Time | ||
} | ||
|
||
// GenericTimeCache provides a generic time cache safe for concurrent use by | ||
// multiple goroutines without additional locking or coordination. | ||
// The implementation is based on the simple TimeCache. | ||
type GenericTimeCache[T any] struct { | ||
// all keys in the cache in the order they were added | ||
// most recent keys are on the front of the indexer; | ||
// it is used to optimize cache sweeping | ||
indexer *list.List | ||
// key in the cache with the value and timestamp it's been added | ||
// to the cache the last time | ||
cache map[string]*genericCacheEntry[T] | ||
// the timespan after which entry in the cache is considered | ||
// as outdated and can be removed from the cache | ||
timespan time.Duration | ||
mutex sync.RWMutex | ||
} | ||
|
||
// NewGenericTimeCache creates a new generic cache instance with provided timespan. | ||
func NewGenericTimeCache[T any](timespan time.Duration) *GenericTimeCache[T] { | ||
return &GenericTimeCache[T]{ | ||
indexer: list.New(), | ||
cache: make(map[string]*genericCacheEntry[T]), | ||
timespan: timespan, | ||
} | ||
} | ||
|
||
// Add adds an entry to the cache. Returns `true` if entry was not present in | ||
// the cache and was successfully added into it. Returns `false` if | ||
// entry is already in the cache. This method is synchronized. | ||
func (tc *GenericTimeCache[T]) Add(key string, value T) bool { | ||
tc.mutex.Lock() | ||
defer tc.mutex.Unlock() | ||
|
||
_, ok := tc.cache[key] | ||
if ok { | ||
return false | ||
} | ||
|
||
tc.sweep() | ||
|
||
tc.cache[key] = &genericCacheEntry[T]{ | ||
value: value, | ||
timestamp: time.Now(), | ||
} | ||
tc.indexer.PushFront(key) | ||
return true | ||
} | ||
|
||
// Get gets an entry from the cache. Boolean flag is `true` if entry is | ||
// present and `false` otherwise. | ||
func (tc *GenericTimeCache[T]) Get(key string) (T, bool) { | ||
tc.mutex.RLock() | ||
defer tc.mutex.RUnlock() | ||
|
||
entry, ok := tc.cache[key] | ||
if !ok { | ||
var zeroValue T | ||
return zeroValue, ok | ||
} | ||
|
||
return entry.value, ok | ||
} | ||
|
||
// Sweep removes old entries. That is those for which caching timespan has | ||
// passed. | ||
func (tc *GenericTimeCache[T]) Sweep() { | ||
tc.mutex.Lock() | ||
defer tc.mutex.Unlock() | ||
|
||
tc.sweep() | ||
} | ||
|
||
func (tc *GenericTimeCache[T]) sweep() { | ||
for { | ||
back := tc.indexer.Back() | ||
if back == nil { | ||
break | ||
} | ||
|
||
key := back.Value.(string) | ||
entry, ok := tc.cache[key] | ||
if !ok { | ||
logger.Errorf( | ||
"inconsistent cache state - expected key [%v] is not present", | ||
key, | ||
) | ||
break | ||
} | ||
|
||
if time.Since(entry.timestamp) > tc.timespan { | ||
tc.indexer.Remove(back) | ||
delete(tc.cache, key) | ||
} else { | ||
break | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package cache | ||
|
||
import ( | ||
"reflect" | ||
"strconv" | ||
"sync" | ||
"testing" | ||
"time" | ||
) | ||
|
||
type valueType struct { | ||
field int | ||
} | ||
|
||
func TestGenericTimeCache_Add(t *testing.T) { | ||
cache := NewGenericTimeCache[*valueType](time.Minute) | ||
|
||
cache.Add("test", &valueType{10}) | ||
|
||
value, ok := cache.Get("test") | ||
if !ok { | ||
t.Fatal("should have 'test' key") | ||
} | ||
|
||
expectedValue := &valueType{10} | ||
if !reflect.DeepEqual(expectedValue, value) { | ||
t.Errorf( | ||
"unexpected value: \n"+ | ||
"exptected: %v\n"+ | ||
"actual: %v", | ||
expectedValue, | ||
value, | ||
) | ||
} | ||
} | ||
|
||
func TestGenericTimeCache_ConcurrentAdd(t *testing.T) { | ||
cache := NewGenericTimeCache[*valueType](time.Minute) | ||
|
||
var wg sync.WaitGroup | ||
wg.Add(10) | ||
|
||
for i := 0; i < 10; i++ { | ||
go func(item int) { | ||
cache.Add(strconv.Itoa(item), &valueType{item}) | ||
wg.Done() | ||
}(i) | ||
} | ||
|
||
wg.Wait() | ||
|
||
for i := 0; i < 10; i++ { | ||
value, ok := cache.Get(strconv.Itoa(i)) | ||
if !ok { | ||
t.Fatalf("should have '%v' key", i) | ||
} | ||
|
||
expectedValue := &valueType{i} | ||
if !reflect.DeepEqual(expectedValue, value) { | ||
t.Errorf( | ||
"unexpected value: \n"+ | ||
"exptected: %v\n"+ | ||
"actual: %v", | ||
expectedValue, | ||
value, | ||
) | ||
} | ||
} | ||
} | ||
|
||
func TestGenericTimeCache_Expiration(t *testing.T) { | ||
cache := NewGenericTimeCache[*valueType](500 * time.Millisecond) | ||
for i := 0; i < 6; i++ { | ||
cache.Add(strconv.Itoa(i), &valueType{i}) | ||
time.Sleep(100 * time.Millisecond) | ||
} | ||
|
||
if _, ok := cache.Get(strconv.Itoa(0)); ok { | ||
t.Fatal("should have dropped '0' key from the cache") | ||
} | ||
} | ||
|
||
func TestGenericTimeCache_Sweep(t *testing.T) { | ||
cache := NewGenericTimeCache[*valueType](500 * time.Millisecond) | ||
cache.Add("old", &valueType{10}) | ||
time.Sleep(100 * time.Millisecond) | ||
cache.Add("new", &valueType{20}) | ||
time.Sleep(400 * time.Millisecond) | ||
|
||
cache.Sweep() | ||
|
||
if _, ok := cache.Get("old"); ok { | ||
t.Fatal("should have dropped 'old' key from the cache") | ||
} | ||
if _, ok := cache.Get("new"); !ok { | ||
t.Fatal("should still have 'new' in the cache") | ||
} | ||
} |