diff --git a/cache.go b/cache.go index af409a3..f3c1f8b 100644 --- a/cache.go +++ b/cache.go @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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 { diff --git a/cache_test.go b/cache_test.go index 259395c..1dd971f 100644 --- a/cache_test.go +++ b/cache_test.go @@ -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) @@ -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 @@ -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)