Skip to content

Commit

Permalink
Merge pull request #121 from keep-network/generic-time-cache
Browse files Browse the repository at this point in the history
Generic time cache
  • Loading branch information
tomaszslabon authored Apr 24, 2024
2 parents f893494 + a8a6659 commit bd36cd2
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 0 deletions.
112 changes: 112 additions & 0 deletions pkg/cache/generic_cache.go
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
}
}
}
98 changes: 98 additions & 0 deletions pkg/cache/generic_cache_test.go
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")
}
}

0 comments on commit bd36cd2

Please sign in to comment.