diff --git a/CHANGELOG.md b/CHANGELOG.md index d502ac8..7ba8bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2.3.0 (February 2021) + +## API changes: + +* #38: Added func (cache *Cache) SetExpirationReasonCallback(callback ExpireReasonCallback) This wil function will replace SetExpirationCallback(..) in the next major version. + +# 2.2.0 (January 2021) + +## API changes: + +* #37 : a GetMetrics call is now available for some information on hits/misses etc. +* #34 : Errors are now const + # 2.1.0 (October 2020) ## API changes diff --git a/Readme.md b/Readme.md index 42636e1..08ea517 100644 --- a/Readme.md +++ b/Readme.md @@ -53,8 +53,9 @@ // all other values are allowed to expire return true } - expirationCallback := func(key string, value interface{}) { - fmt.Printf("This key(%s) has expired\n", key) + + expirationCallback := func(key string, reason ttlcache.EvictionReason, value interface{}) { + fmt.Printf("This key(%s) has expired because of %s\n", key, reason) } loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) { @@ -65,12 +66,12 @@ } cache := ttlcache.NewCache() - defer cache.Close() cache.SetTTL(time.Duration(10 * time.Second)) - cache.SetExpirationCallback(expirationCallback) + cache.SetExpirationReasonCallback(expirationCallback) cache.SetLoaderFunction(loaderFunction) cache.SetNewItemCallback(newItemCallback) cache.SetCheckExpirationCallback(checkExpirationCallback) + cache.SetCacheSizeLimit(2) cache.Set("key", "value") cache.SetWithTTL("keyWithTTL", "value", 10*time.Second) @@ -82,6 +83,14 @@ if result := cache.Remove("keyNNN"); result == notFound { fmt.Printf("Not found, %d items left\n", count) } + + cache.Set("key6", "value") + cache.Set("key7", "value") + metrics := cache.GetMetrics() + fmt.Printf("Total inserted: %d\n", metrics.Inserted) + + cache.Close() + } func getFromNetwork(key string) (string, error) { diff --git a/cache.go b/cache.go index 80fa165..89dd917 100644 --- a/cache.go +++ b/cache.go @@ -9,7 +9,11 @@ type CheckExpireCallback func(key string, value interface{}) bool // ExpireCallback is used as a callback on item expiration or when notifying of an item new to the cache +// Note that ExpireReasonCallback will be the succesor of this function in the next major release. type ExpireCallback func(key string, value interface{}) + +// ExpireReasonCallback is used as a callback on item expiration with extra information why the item expired. +type ExpireReasonCallback func(key string, reason EvictionReason, value interface{}) // LoaderFunction can be supplied to retrieve an item where a cache miss occurs. Supply an item specific ttl or Duration.Zero type LoaderFunction func(key string) (data interface{}, ttl time.Duration, err error) @@ -21,6 +25,7 @@ items map[string]*item loaderLock map[string]*sync.Cond expireCallback ExpireCallback + expireReasonCallback ExpireReasonCallback checkExpireCallback CheckExpireCallback newItemCallback ExpireCallback priorityQueue *priorityQueue @@ -34,6 +39,20 @@ metrics Metrics } +// EvictionReason is an enum that explains why an item was evicted +type EvictionReason int + +const ( + // Removed : explicitly removed from cache via API call + Removed EvictionReason = iota + // EvictedSize : evicted due to exceeding the cache size + EvictedSize + // Expired : the time to live is zero and therefore the item is removed + Expired + // Closed : the cache was closed + Closed +) + const ( // ErrClosed is raised when operating on a cache where Close() has already been called. ErrClosed = constError("cache already closed") @@ -51,7 +70,6 @@ item, exists := cache.items[key] if !exists || item.expired() { return nil, false, false - } else { } if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) { @@ -103,7 +121,7 @@ timer.Stop() cache.mutex.Lock() if cache.priorityQueue.Len() > 0 { - cache.evictjob() + cache.evictjob(Closed) } cache.mutex.Unlock() shutdownFeedback <- struct{}{} @@ -126,22 +144,29 @@ } } -func (cache *Cache) removeItem(item *item) { - cache.metrics.Evicted++ +func (cache *Cache) checkExpirationCallback(item *item, reason EvictionReason) { if cache.expireCallback != nil { go cache.expireCallback(item.key, item.data) } + if cache.expireReasonCallback != nil { + go cache.expireReasonCallback(item.key, reason, item.data) + } +} + +func (cache *Cache) removeItem(item *item, reason EvictionReason) { + cache.metrics.Evicted++ + cache.checkExpirationCallback(item, reason) cache.priorityQueue.remove(item) delete(cache.items, item.key) } -func (cache *Cache) evictjob() { +func (cache *Cache) evictjob(reason EvictionReason) { // index will only be advanced if the current entry will not be evicted i := 0 for item := cache.priorityQueue.items[i]; ; item = cache.priorityQueue.items[i] { - cache.removeItem(item) + cache.removeItem(item, reason) if cache.priorityQueue.Len() == 0 { return } @@ -165,7 +190,7 @@ } } - cache.removeItem(item) + cache.removeItem(item, Expired) if cache.priorityQueue.Len() == 0 { return } @@ -210,7 +235,7 @@ item.ttl = ttl } else { if cache.sizeLimit != 0 && len(cache.items) >= cache.sizeLimit { - cache.removeItem(cache.priorityQueue.items[0]) + cache.removeItem(cache.priorityQueue.items[0], EvictedSize) } item = newItem(key, data, ttl) cache.items[key] = item @@ -327,7 +352,7 @@ if !exists { return ErrNotFound } - cache.removeItem(object) + cache.removeItem(object, Removed) return nil } @@ -361,6 +386,11 @@ // SetExpirationCallback sets a callback that will be called when an item expires func (cache *Cache) SetExpirationCallback(callback ExpireCallback) { cache.expireCallback = callback +} + +// SetExpirationReasonCallback sets a callback that will be called when an item expires, includes reason of expiry +func (cache *Cache) SetExpirationReasonCallback(callback ExpireReasonCallback) { + cache.expireReasonCallback = callback } // SetCheckExpirationCallback sets a callback that will be called when an item is about to expire diff --git a/cache_test.go b/cache_test.go index ca9e11f..235a009 100644 --- a/cache_test.go +++ b/cache_test.go @@ -18,6 +18,43 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m) +} + +// Issue #38: Feature request: ability to know why an expiry has occurred +func TestCache_textExpirationReasons(t *testing.T) { + t.Parallel() + cache := NewCache() + + var reason EvictionReason + var sync = make(chan struct{}) + expirationReason := func(key string, evReason EvictionReason, value interface{}) { + reason = evReason + sync <- struct{}{} + } + cache.SetExpirationReasonCallback(expirationReason) + + cache.SetTTL(time.Millisecond) + cache.Set("one", "one") + <-sync + assert.Equal(t, Expired, reason) + + cache.SetTTL(time.Hour) + cache.SetCacheSizeLimit(1) + cache.Set("two", "two") + cache.Set("twoB", "twoB") + <-sync + assert.Equal(t, EvictedSize, reason) + + cache.Remove("twoB") + <-sync + assert.Equal(t, Removed, reason) + + cache.SetTTL(time.Hour) + cache.Set("three", "three") + cache.Close() + <-sync + assert.Equal(t, Closed, reason) + } // Issue #37: Cache metrics diff --git a/evictionreason_enumer.go b/evictionreason_enumer.go new file mode 100644 index 0000000..dcff95d --- /dev/null +++ b/evictionreason_enumer.go @@ -0,0 +1,52 @@ +// Code generated by "enumer -type EvictionReason"; DO NOT EDIT. + +// +package ttlcache + +import ( + "fmt" +) + +const _EvictionReasonName = "RemovedEvictedSizeExpiredClosed" + +var _EvictionReasonIndex = [...]uint8{0, 7, 18, 25, 31} + +func (i EvictionReason) String() string { + if i < 0 || i >= EvictionReason(len(_EvictionReasonIndex)-1) { + return fmt.Sprintf("EvictionReason(%d)", i) + } + return _EvictionReasonName[_EvictionReasonIndex[i]:_EvictionReasonIndex[i+1]] +} + +var _EvictionReasonValues = []EvictionReason{0, 1, 2, 3} + +var _EvictionReasonNameToValueMap = map[string]EvictionReason{ + _EvictionReasonName[0:7]: 0, + _EvictionReasonName[7:18]: 1, + _EvictionReasonName[18:25]: 2, + _EvictionReasonName[25:31]: 3, +} + +// EvictionReasonString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func EvictionReasonString(s string) (EvictionReason, error) { + if val, ok := _EvictionReasonNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to EvictionReason values", s) +} + +// EvictionReasonValues returns all values of the enum +func EvictionReasonValues() []EvictionReason { + return _EvictionReasonValues +} + +// IsAEvictionReason returns "true" if the value is listed in the enum definition. "false" otherwise +func (i EvictionReason) IsAEvictionReason() bool { + for _, v := range _EvictionReasonValues { + if i == v { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index b5cee44..510ef4c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ go 1.15 require ( + github.com/alvaroloes/enumer v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/stretchr/testify v1.7.0 go.uber.org/goleak v1.1.10 diff --git a/go.sum b/go.sum index d6ffb20..168c7af 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY= +github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -12,6 +14,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= @@ -55,6 +59,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=