diff --git a/datastore/cache.go b/datastore/cache.go index 43062417b..3f8e50730 100644 --- a/datastore/cache.go +++ b/datastore/cache.go @@ -43,6 +43,14 @@ func (cache *MemCache) lockForKey(key string) *locksutil.LockEntry { return locksutil.LockForKey(cache.locks, key) } +func (cache *MemCache) set(key string, value interface{}, ttl time.Duration) { + if cache.hasEviction { + // Delete first to ensure eviction is handled. + cache.instance.Delete(key) + } + cache.instance.Set(key, value, ttl) +} + // GetOrUpdate attempts to retrieve the value stored at the given key. // If the value does not exist, then it generates the value using // the generateValue function and stores it with the specified ttl. @@ -57,7 +65,7 @@ func (cache *MemCache) GetOrUpdate(key string, ttl time.Duration, resetTTLOnHit log.Tracef("Cache hit on %v", key) if resetTTLOnHit { // Update last-access time - cache.instance.Set(key, value, ttl) + cache.set(key, value, ttl) } if err, ok := value.(error); ok { return nil, err @@ -71,15 +79,15 @@ func (cache *MemCache) GetOrUpdate(key string, ttl time.Duration, resetTTLOnHit // Cache error responses as well. These are often authentication or availability failures // and we don't want to continually query the API on failures. if err != nil { - cache.instance.Set(key, err, ttl) + cache.set(key, err, ttl) return nil, err } - cache.instance.Set(key, value, ttl) + cache.set(key, value, ttl) return value, nil } -// Flush deletes all items from the cache. +// Flush deletes all items from the cache. Also resets cache capacity. // This operation is significantly slower when cache was created with NewMemCacheWithEvicted. func (cache *MemCache) Flush() { if cache.hasEviction { diff --git a/datastore/cache_test.go b/datastore/cache_test.go index 06b2a17b3..d084735b7 100644 --- a/datastore/cache_test.go +++ b/datastore/cache_test.go @@ -1,22 +1,113 @@ package datastore import ( + "errors" "regexp" "testing" + "time" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) +const anything = "anything" + type MemCacheTestSuite struct { suite.Suite - mem *MemCache + mem *MemCache + thing mock.Mock } func (suite *MemCacheTestSuite) SetupTest() { suite.mem = NewMemCache() + suite.thing = mock.Mock{} +} + +func (suite *MemCacheTestSuite) update() (interface{}, error) { + args := suite.thing.Called() + return args.Get(0), args.Error(1) +} + +func (suite *MemCacheTestSuite) validate(item interface{}, err error) { + if suite.Nil(err) { + suite.Equal(anything, item) + } +} + +func (suite *MemCacheTestSuite) TestGetOrUpdateNoReset() { + suite.thing.On("update").Return(anything, nil) + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Second, false, suite.update)) + item, ok := suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Nanosecond, false, suite.update)) + time.Sleep(time.Nanosecond) + item, ok = suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + suite.thing.AssertNumberOfCalls(suite.T(), "update", 1) + + suite.mem.instance.Delete("an entry") + suite.validate(suite.mem.GetOrUpdate("an entry", time.Second, false, suite.update)) + item, ok = suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + suite.thing.AssertNumberOfCalls(suite.T(), "update", 2) +} + +func (suite *MemCacheTestSuite) TestGetOrUpdateExpire() { + suite.thing.On("update").Return(anything, nil) + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Nanosecond, false, suite.update)) + time.Sleep(time.Nanosecond) + _, ok := suite.mem.instance.Get("an entry") + suite.False(ok) + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Second, false, suite.update)) + item, ok := suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + suite.thing.AssertNumberOfCalls(suite.T(), "update", 2) } -func (suite *MemCacheTestSuite) TestClearCache() { +func (suite *MemCacheTestSuite) TestGetOrUpdateWithReset() { + suite.thing.On("update").Return(anything, nil) + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Second, true, suite.update)) + item, ok := suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Nanosecond, true, suite.update)) + time.Sleep(time.Nanosecond) + _, ok = suite.mem.instance.Get("an entry") + suite.False(ok) + suite.thing.AssertNumberOfCalls(suite.T(), "update", 1) + + suite.validate(suite.mem.GetOrUpdate("an entry", time.Second, true, suite.update)) + item, ok = suite.mem.instance.Get("an entry") + if suite.True(ok) { + suite.Equal("anything", item) + } + suite.thing.AssertNumberOfCalls(suite.T(), "update", 2) +} + +func (suite *MemCacheTestSuite) TestFlush() { + suite.mem.instance.Set("an entry", struct{}{}, time.Nanosecond) + time.Sleep(time.Nanosecond) + suite.mem.instance.SetDefault("another entry", struct{}{}) + suite.mem.Flush() + suite.Equal(0, suite.mem.instance.ItemCount()) +} + +func (suite *MemCacheTestSuite) TestDelete() { suite.mem.instance.SetDefault("an entry", struct{}{}) suite.mem.instance.SetDefault("another entry", struct{}{}) suite.NotNil(suite.mem.instance.Get("an entry")) @@ -33,3 +124,65 @@ func (suite *MemCacheTestSuite) TestClearCache() { func TestMemCache(t *testing.T) { suite.Run(t, new(MemCacheTestSuite)) } + +type MemCacheEvictedTestSuite struct { + suite.Suite + mem *MemCache + evictor mock.Mock +} + +func (suite *MemCacheEvictedTestSuite) evict(s string, i interface{}) { + suite.evictor.Called(s, i) +} + +func (suite *MemCacheEvictedTestSuite) SetupTest() { + suite.mem = NewMemCacheWithEvicted(suite.evict) + suite.evictor = mock.Mock{} +} + +func (suite *MemCacheEvictedTestSuite) TestFlush() { + suite.mem.instance.Set("an entry", struct{}{}, time.Nanosecond) + time.Sleep(time.Nanosecond) + suite.mem.instance.SetDefault("another entry", struct{}{}) + + suite.evictor.On("evict", "an entry", mock.Anything) + suite.evictor.On("evict", "another entry", mock.Anything) + suite.mem.Flush() + suite.Equal(0, suite.mem.instance.ItemCount()) + suite.evictor.AssertExpectations(suite.T()) +} + +func (suite *MemCacheEvictedTestSuite) TestExpired() { + suite.mem.instance.Set("an entry", struct{}{}, time.Nanosecond) + time.Sleep(time.Nanosecond) + + suite.evictor.On("evict", "an entry", mock.Anything) + _, err := suite.mem.GetOrUpdate("an entry", time.Second, false, func() (interface{}, error) { + return nil, errors.New("nope") + }) + suite.Equal(errors.New("nope"), err) + if val, ok := suite.mem.instance.Get("an entry"); suite.True(ok) { + suite.Equal(errors.New("nope"), val) + } + suite.evictor.AssertExpectations(suite.T()) +} + +func (suite *MemCacheEvictedTestSuite) TestDelete() { + suite.mem.instance.SetDefault("an entry", struct{}{}) + suite.mem.instance.SetDefault("another entry", struct{}{}) + + matcher, err := regexp.Compile("^.*n e.*$") + suite.Nil(err) + + suite.evictor.On("evict", "an entry", mock.Anything) + deleted := suite.mem.Delete(matcher) + suite.Equal([]string{"an entry"}, deleted) + suite.evictor.AssertExpectations(suite.T()) + + suite.Nil(suite.mem.instance.Get("an entry")) + suite.NotNil(suite.mem.instance.Get("another entry")) +} + +func TestMemCacheEvicted(t *testing.T) { + suite.Run(t, new(MemCacheEvictedTestSuite)) +}