diff --git a/v2/cache.go b/v2/cache.go index b934691..d340573 100644 --- a/v2/cache.go +++ b/v2/cache.go @@ -13,10 +13,10 @@ package cache import ( - "container/list" "fmt" - "sync" "time" + + v3 "github.com/go-pkgz/expirable-cache/v3" ) // Cache defines cache interface @@ -44,225 +44,49 @@ type Stats struct { // cacheImpl provides Cache interface implementation. type cacheImpl[K comparable, V any] struct { - ttl time.Duration - maxKeys int - isLRU bool - onEvicted func(key K, value V) - - sync.Mutex - stat Stats - items map[K]*list.Element - evictList *list.List + v3.Cache[K, V] } -// noEvictionTTL - very long ttl to prevent eviction -const noEvictionTTL = time.Hour * 24 * 365 * 10 - // NewCache returns a new Cache. // Default MaxKeys is unlimited (0). // Default TTL is 10 years, sane value for expirable cache is 5 minutes. // Default eviction mode is LRC, appropriate option allow to change it to LRU. func NewCache[K comparable, V any]() Cache[K, V] { - return &cacheImpl[K, V]{ - items: map[K]*list.Element{}, - evictList: list.New(), - ttl: noEvictionTTL, - maxKeys: 0, - } + return &cacheImpl[K, V]{v3.NewCache[K, V]()} } -// Set key, ttl of 0 would use cache-wide TTL -func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { - c.Lock() - defer c.Unlock() - now := time.Now() - if ttl == 0 { - ttl = c.ttl - } - - // Check for existing item - if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) - ent.Value.(*cacheItem[K, V]).value = value - ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl) - return - } - - // Add new item - ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)} - entry := c.evictList.PushFront(ent) - c.items[key] = entry - c.stat.Added++ - - // Remove oldest entry if it is expired, only in case of non-default TTL. - if c.ttl != noEvictionTTL || ttl != noEvictionTTL { - c.removeOldestIfExpired() - } - - // Verify size not exceeded - if c.maxKeys > 0 && len(c.items) > c.maxKeys { - c.removeOldest() - } -} - -// Get returns the key value if it's not expired func (c *cacheImpl[K, V]) Get(key K) (V, bool) { - def := *new(V) - c.Lock() - defer c.Unlock() - if ent, ok := c.items[key]; ok { - // Expired item check - if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { - c.stat.Misses++ - return def, false - } - if c.isLRU { - c.evictList.MoveToFront(ent) - } - c.stat.Hits++ - return ent.Value.(*cacheItem[K, V]).value, true + value, ok := c.Cache.Get(key) + if !ok { + // preserve v2 behaviour of not returning value in case it's expired + // which is not compatible with v3 and simplelru + def := *new(V) + return def, ok } - c.stat.Misses++ - return def, false + return value, ok } -// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key. -// Works exactly the same as Get in case of LRC mode (default one). func (c *cacheImpl[K, V]) Peek(key K) (V, bool) { - def := *new(V) - c.Lock() - defer c.Unlock() - if ent, ok := c.items[key]; ok { - // Expired item check - if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { - c.stat.Misses++ - return def, false - } - c.stat.Hits++ - return ent.Value.(*cacheItem[K, V]).value, true + value, ok := c.Cache.Peek(key) + if !ok { + // preserve v2 behaviour of not returning value in case it's expired + // which is not compatible with v3 and simplelru + def := *new(V) + return def, ok } - c.stat.Misses++ - return def, false -} - -// Keys returns a slice of the keys in the cache, from oldest to newest. -func (c *cacheImpl[K, V]) Keys() []K { - c.Lock() - defer c.Unlock() - return c.keys() -} - -// Len return count of items in cache, including expired -func (c *cacheImpl[K, V]) Len() int { - c.Lock() - defer c.Unlock() - return c.evictList.Len() + return value, ok } -// Invalidate key (item) from the cache -func (c *cacheImpl[K, V]) Invalidate(key K) { - c.Lock() - defer c.Unlock() - if ent, ok := c.items[key]; ok { - c.removeElement(ent) - } -} - -// InvalidateFn deletes multiple keys if predicate is true -func (c *cacheImpl[K, V]) InvalidateFn(fn func(key K) bool) { - c.Lock() - defer c.Unlock() - for key, ent := range c.items { - if fn(key) { - c.removeElement(ent) - } - } -} - -// RemoveOldest remove oldest element in the cache func (c *cacheImpl[K, V]) RemoveOldest() { - c.Lock() - defer c.Unlock() - c.removeOldest() -} - -// DeleteExpired clears cache of expired items -func (c *cacheImpl[K, V]) DeleteExpired() { - c.Lock() - defer c.Unlock() - for _, key := range c.keys() { - if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) { - c.removeElement(c.items[key]) - } - } + c.Cache.RemoveOldest() } -// Purge clears the cache completely. -func (c *cacheImpl[K, V]) Purge() { - c.Lock() - defer c.Unlock() - for k, v := range c.items { - delete(c.items, k) - c.stat.Evicted++ - if c.onEvicted != nil { - c.onEvicted(k, v.Value.(*cacheItem[K, V]).value) - } - } - c.evictList.Init() -} - -// Stat gets the current stats for cache func (c *cacheImpl[K, V]) Stat() Stats { - c.Lock() - defer c.Unlock() - return c.stat -} - -func (c *cacheImpl[K, V]) String() string { - stats := c.Stat() - size := c.Len() - return fmt.Sprintf("Size: %d, Stats: %+v (%0.1f%%)", size, stats, 100*float64(stats.Hits)/float64(stats.Hits+stats.Misses)) -} - -// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock! -func (c *cacheImpl[K, V]) keys() []K { - keys := make([]K, 0, len(c.items)) - for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() { - keys = append(keys, ent.Value.(*cacheItem[K, V]).key) + stats := c.Cache.Stat() + return Stats{ + Hits: stats.Hits, + Misses: stats.Misses, + Added: stats.Added, + Evicted: stats.Evicted, } - return keys -} - -// removeOldest removes the oldest item from the cache. Has to be called with lock! -func (c *cacheImpl[K, V]) removeOldest() { - ent := c.evictList.Back() - if ent != nil { - c.removeElement(ent) - } -} - -// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock! -func (c *cacheImpl[K, V]) removeOldestIfExpired() { - ent := c.evictList.Back() - if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { - c.removeElement(ent) - } -} - -// removeElement is used to remove a given list element from the cache. Has to be called with lock! -func (c *cacheImpl[K, V]) removeElement(e *list.Element) { - c.evictList.Remove(e) - kv := e.Value.(*cacheItem[K, V]) - delete(c.items, kv.key) - c.stat.Evicted++ - if c.onEvicted != nil { - c.onEvicted(kv.key, kv.value) - } -} - -// cacheItem is used to hold a value in the evictList -type cacheItem[K comparable, V any] struct { - expiresAt time.Time - key K - value V } diff --git a/v2/go.mod b/v2/go.mod index 558cc25..71f3a03 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -2,10 +2,15 @@ module github.com/go-pkgz/expirable-cache/v2 go 1.20 -require github.com/stretchr/testify v1.8.4 +require ( + github.com/go-pkgz/expirable-cache/v3 v3.0.0 + github.com/stretchr/testify v1.8.4 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/go-pkgz/expirable-cache/v3 => ../v3 diff --git a/v2/go.sum b/v2/go.sum index fa4b6e6..c3f5969 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,5 +1,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/v2/options.go b/v2/options.go index b5fe65d..fc1a0f7 100644 --- a/v2/options.go +++ b/v2/options.go @@ -12,25 +12,25 @@ type options[K comparable, V any] interface { // WithTTL functional option defines TTL for all cache entries. // By default, it is set to 10 years, sane option for expirable cache might be 5 minutes. func (c *cacheImpl[K, V]) WithTTL(ttl time.Duration) Cache[K, V] { - c.ttl = ttl + c.Cache.WithTTL(ttl) return c } // WithMaxKeys functional option defines how many keys to keep. // By default, it is 0, which means unlimited. func (c *cacheImpl[K, V]) WithMaxKeys(maxKeys int) Cache[K, V] { - c.maxKeys = maxKeys + c.Cache.WithMaxKeys(maxKeys) return c } // WithLRU sets cache to LRU (Least Recently Used) eviction mode. func (c *cacheImpl[K, V]) WithLRU() Cache[K, V] { - c.isLRU = true + c.Cache.WithLRU() return c } // WithOnEvicted defined function which would be called automatically for automatically and manually deleted entries func (c *cacheImpl[K, V]) WithOnEvicted(fn func(key K, value V)) Cache[K, V] { - c.onEvicted = fn + c.Cache.WithOnEvicted(fn) return c }