Skip to content
This repository was archived by the owner on Jun 2, 2022. It is now read-only.
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
16 changes: 12 additions & 4 deletions datastore/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 {
Expand Down
157 changes: 155 additions & 2 deletions datastore/cache_test.go
Original file line number Diff line number Diff line change
@@ -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"))
Expand All @@ -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))
}