Skip to content

Commit

Permalink
Add refresh cache functionality in agecache (#22)
Browse files Browse the repository at this point in the history
* Add refresh cache functionality in agecache

* Add background refresh functionality

* Background refresh only when the new map is not nil

---------

Co-authored-by: Nithin Benny <[email protected]>
  • Loading branch information
Nithin4181 and Nithin Benny authored Sep 30, 2024
1 parent 96f15d1 commit 3c8cb45
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 4 deletions.
63 changes: 59 additions & 4 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import (
//
// The struct supports stats package tags, example:
//
// prev := cache.Stats()
// s := cache.Stats().Delta(prev)
// stats.WithPrefix("mycache").Observe(s)
//
// prev := cache.Stats()
// s := cache.Stats().Delta(prev)
// stats.WithPrefix("mycache").Observe(s)
type Stats struct {
Capacity int64 `metric:"capacity" type:"gauge"` // Gauge, maximum capacity for the cache
Count int64 `metric:"count" type:"gauge"` // Gauge, number of items in the cache
Expand Down Expand Up @@ -80,6 +79,12 @@ type Config struct {
OnEviction func(key, value interface{})
// Optional callback invoked when an item expired
OnExpiration func(key, value interface{})
// Optional refresh interval after which all items in the cache expires.
// If zero, refreshing cache is disabled.
RefreshInterval time.Duration
// Optional on refresh callback invoked when the cache is refreshed
// Both RefreshInterval and OnRefresh must be provided to enable background cache refresh
OnRefresh func() map[interface{}]interface{}
}

// Entry pointed to by each list.Element
Expand Down Expand Up @@ -134,6 +139,10 @@ func New(config Config) *Cache {
panic("config.MinAge must be less than or equal to config.MaxAge")
}

if config.RefreshInterval < 0 {
panic("Must supply a zero or positive config.RefreshInterval")
}

minAge := config.MinAge
if minAge == 0 {
minAge = config.MaxAge
Expand Down Expand Up @@ -167,6 +176,22 @@ func New(config Config) *Cache {
}()
}

if config.RefreshInterval > 0 && config.OnRefresh != nil {
cache.RefreshCache(config.OnRefresh())
go func() {
t := time.NewTicker(config.RefreshInterval)
defer t.Stop()
for {
<-t.C
items := config.OnRefresh()
// Only refresh the cache if the items provided is not nil
if items != nil {
cache.RefreshCache(items)
}
}
}()
}

return cache
}

Expand Down Expand Up @@ -228,6 +253,36 @@ func (cache *Cache) Get(key interface{}) (interface{}, bool) {
return nil, false
}

// RefreshCache refreshes the entire cache with the new items map
func (cache *Cache) RefreshCache(items map[interface{}]interface{}) {
cache.mutex.Lock()
defer cache.mutex.Unlock()

cache.items = make(map[interface{}]*list.Element)
cache.evictionList.Init()

for key, value := range items {
cache.sets++
timestamp := cache.getTimestamp()

if element, ok := cache.items[key]; ok {
cache.evictionList.MoveToFront(element)
entry := element.Value.(*cacheEntry)
entry.value = value
entry.timestamp = timestamp
}

entry := &cacheEntry{key, value, timestamp}
element := cache.evictionList.PushFront(entry)
cache.items[key] = element

evict := cache.evictionList.Len() > cache.capacity
if evict {
cache.evictOldest()
}
}
}

// Has returns whether or not the `key` is in the cache without updating
// how recently it was accessed or deleting it for having expired.
func (cache *Cache) Has(key interface{}) bool {
Expand Down
90 changes: 90 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func TestInvalidMinAge(t *testing.T) {
})
}

func TestInvalidRefreshInterval(t *testing.T) {
assert.Panics(t, func() {
New(Config{Capacity: 1, RefreshInterval: -1 * time.Hour})
})
}

func TestBasicSetGet(t *testing.T) {
cache := New(Config{Capacity: 2})
cache.Set("foo", 1)
Expand Down Expand Up @@ -109,6 +115,67 @@ func TestExpiration(t *testing.T) {
assert.False(t, eviction)
}

func TestCacheBackgroundRefresh(t *testing.T) {
count := 0
cache := New(Config{
Capacity: 1,
RefreshInterval: 3 * time.Second,
OnRefresh: func() map[interface{}]interface{} {
count++
return map[interface{}]interface{}{"key": count}
},
})

value, ok := cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 1, value)

time.Sleep(4 * time.Second) // wait for the refresh loop to run

value, ok = cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 2, value)

time.Sleep(4 * time.Second)
value, ok = cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 3, value)

}

func TestCacheBackgroundRefreshForNilData(t *testing.T) {
count := 0
cache := New(Config{
Capacity: 1,
RefreshInterval: 3 * time.Second,
OnRefresh: func() map[interface{}]interface{} {
count++

if count == 2 {
return nil
}
return map[interface{}]interface{}{"key": count}
},
})

value, ok := cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 1, value)

time.Sleep(4 * time.Second)

// Prevent refresh when the OnRefresh call back returns nil
value, ok = cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 1, value)

time.Sleep(4 * time.Second)
value, ok = cache.Get("key")
assert.Equal(t, true, ok)
assert.Equal(t, 3, value)

}

type MockRandGenerator struct {
startAt int64
incr int64
Expand Down Expand Up @@ -256,6 +323,29 @@ func TestClear(t *testing.T) {
assert.Equal(t, 0, cache.Len())
}

func TestRefreshCache(t *testing.T) {
cache := New(Config{Capacity: 10})
cache.Set("foo", 1)
cache.Set("bar", 2)

refreshedCacheEntries := map[interface{}]interface{}{}
for i := 0; i <= 9; i++ {
refreshedCacheEntries[i] = i
}
cache.RefreshCache(refreshedCacheEntries)

assert.False(t, cache.Has("foo"))
assert.False(t, cache.Has("bar"))

for i := 0; i <= 9; i++ {
_, ok := cache.Get(i)
assert.True(t, ok)
}

assert.Equal(t, 10, cache.Len())

}

func TestKeys(t *testing.T) {
cache := New(Config{Capacity: 10})
cache.Set("foo", 1)
Expand Down

0 comments on commit 3c8cb45

Please sign in to comment.