Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refresh cache functionality in agecache #22

Merged
merged 3 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading