From 7aceec43cf19be18d1323edad836c4b57d3890b4 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Tue, 20 Feb 2024 16:39:58 +0100 Subject: [PATCH 1/3] v3 initial copy from v2 --- .github/workflows/ci-v3.yml | 48 +++++++ .github/workflows/ci.yml | 4 + v3/cache.go | 268 ++++++++++++++++++++++++++++++++++++ v3/cache_test.go | 212 ++++++++++++++++++++++++++++ v3/go.mod | 11 ++ v3/go.sum | 10 ++ v3/options.go | 36 +++++ 7 files changed, 589 insertions(+) create mode 100644 .github/workflows/ci-v3.yml create mode 100644 v3/cache.go create mode 100644 v3/cache_test.go create mode 100644 v3/go.mod create mode 100644 v3/go.sum create mode 100644 v3/options.go diff --git a/.github/workflows/ci-v3.yml b/.github/workflows/ci-v3.yml new file mode 100644 index 0000000..b9509d0 --- /dev/null +++ b/.github/workflows/ci-v3.yml @@ -0,0 +1,48 @@ +name: build-v3 + +on: + push: + branches: + tags: + paths: + - ".github/workflows/ci-v3.yml" + - "v3/**" + pull_request: + paths: + - ".github/workflows/ci-v3.yml" + - "v3/**" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: "1.20" + id: go + + - name: checkout + uses: actions/checkout@v4 + + - name: build and test + run: | + go test -timeout=60s -race -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov + go build -race + working-directory: v3 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --config ../.golangci.yml + working-directory: v3 + + - name: install goveralls, submit coverage + run: | + go install github.com/mattn/goveralls@latest + goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5993872..9d4c0c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,15 @@ on: tags: paths-ignore: - ".github/workflows/ci-v2.yml" + - ".github/workflows/ci-v3.yml" - "v2/**" + - "v3/**" pull_request: paths-ignore: - ".github/workflows/ci-v2.yml" + - ".github/workflows/ci-v3.yml" - "v2/**" + - "v3/**" jobs: build: diff --git a/v3/cache.go b/v3/cache.go new file mode 100644 index 0000000..b934691 --- /dev/null +++ b/v3/cache.go @@ -0,0 +1,268 @@ +// Package cache implements Cache similar to hashicorp/golang-lru +// +// Support LRC, LRU and TTL-based eviction. +// Package is thread-safe and doesn't spawn any goroutines. +// On every Set() call, cache deletes single oldest entry in case it's expired. +// In case MaxSize is set, cache deletes the oldest entry disregarding its expiration date to maintain the size, +// either using LRC or LRU eviction. +// In case of default TTL (10 years) and default MaxSize (0, unlimited) the cache will be truly unlimited +// and will never delete entries from itself automatically. +// +// Important: only reliable way of not having expired entries stuck in a cache is to +// run cache.DeleteExpired periodically using time.Ticker, advisable period is 1/2 of TTL. +package cache + +import ( + "container/list" + "fmt" + "sync" + "time" +) + +// Cache defines cache interface +type Cache[K comparable, V any] interface { + fmt.Stringer + options[K, V] + Set(key K, value V, ttl time.Duration) + Get(key K) (V, bool) + Peek(key K) (V, bool) + Keys() []K + Len() int + Invalidate(key K) + InvalidateFn(fn func(key K) bool) + RemoveOldest() + DeleteExpired() + Purge() + Stat() Stats +} + +// Stats provides statistics for cache +type Stats struct { + Hits, Misses int // cache effectiveness + Added, Evicted int // number of added and evicted records +} + +// 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 +} + +// 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, + } +} + +// 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 + } + c.stat.Misses++ + return def, false +} + +// 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 + } + 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() +} + +// 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]) + } + } +} + +// 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) + } + 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/v3/cache_test.go b/v3/cache_test.go new file mode 100644 index 0000000..49b23c6 --- /dev/null +++ b/v3/cache_test.go @@ -0,0 +1,212 @@ +package cache + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCacheNoPurge(t *testing.T) { + lc := NewCache[string, string]() + + lc.Set("key1", "val1", 0) + assert.Equal(t, 1, lc.Len()) + + v, ok := lc.Peek("key1") + assert.Equal(t, "val1", v) + assert.True(t, ok) + + v, ok = lc.Peek("key2") + assert.Empty(t, v) + assert.False(t, ok) + + assert.Equal(t, []string{"key1"}, lc.Keys()) +} + +func TestCacheWithDeleteExpired(t *testing.T) { + var evicted []string + lc := NewCache[string, string]().WithTTL(150 * time.Millisecond).WithOnEvicted( + func(key string, value string) { + evicted = append(evicted, key, value) + }) + + lc.Set("key1", "val1", 0) + + time.Sleep(100 * time.Millisecond) // not enough to expire + lc.DeleteExpired() + assert.Equal(t, 1, lc.Len()) + + v, ok := lc.Get("key1") + assert.Equal(t, "val1", v) + assert.True(t, ok) + + time.Sleep(200 * time.Millisecond) // expire + lc.DeleteExpired() + v, ok = lc.Get("key1") + assert.False(t, ok) + assert.Equal(t, "", v) + + assert.Equal(t, 0, lc.Len()) + assert.Equal(t, []string{"key1", "val1"}, evicted) + + // add new entry + lc.Set("key2", "val2", 0) + assert.Equal(t, 1, lc.Len()) + + // nothing deleted + lc.DeleteExpired() + assert.Equal(t, 1, lc.Len()) + assert.Equal(t, []string{"key1", "val1"}, evicted) + + // Purge, cache should be clean + lc.Purge() + assert.Equal(t, 0, lc.Len()) + assert.Equal(t, []string{"key1", "val1", "key2", "val2"}, evicted) +} + +func TestCacheWithPurgeEnforcedBySize(t *testing.T) { + lc := NewCache[string, string]().WithTTL(time.Hour).WithMaxKeys(10) + + for i := 0; i < 100; i++ { + i := i + lc.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i), 0) + v, ok := lc.Get(fmt.Sprintf("key%d", i)) + assert.Equal(t, fmt.Sprintf("val%d", i), v) + assert.True(t, ok) + assert.True(t, lc.Len() < 20) + } + + assert.Equal(t, 10, lc.Len()) +} + +func TestCacheConcurrency(t *testing.T) { + lc := NewCache[string, string]() + wg := sync.WaitGroup{} + wg.Add(1000) + for i := 0; i < 1000; i++ { + go func(i int) { + lc.Set(fmt.Sprintf("key-%d", i/10), fmt.Sprintf("val-%d", i/10), 0) + wg.Done() + }(i) + } + wg.Wait() + assert.Equal(t, 100, lc.Len()) +} + +func TestCacheInvalidateAndEvict(t *testing.T) { + var evicted int + lc := NewCache[string, string]().WithLRU().WithOnEvicted(func(_ string, _ string) { evicted++ }) + + lc.Set("key1", "val1", 0) + lc.Set("key2", "val2", 0) + + val, ok := lc.Get("key1") + assert.True(t, ok) + assert.Equal(t, "val1", val) + assert.Equal(t, 0, evicted) + + lc.Invalidate("key1") + assert.Equal(t, 1, evicted) + val, ok = lc.Get("key1") + assert.Empty(t, val) + assert.False(t, ok) + + val, ok = lc.Get("key2") + assert.True(t, ok) + assert.Equal(t, "val2", val) + + lc.InvalidateFn(func(key string) bool { + return key == "key2" + }) + assert.Equal(t, 2, evicted) + _, ok = lc.Get("key2") + assert.False(t, ok) + assert.Equal(t, 0, lc.Len()) +} + +func TestCacheExpired(t *testing.T) { + lc := NewCache[string, string]().WithTTL(time.Millisecond * 5) + + lc.Set("key1", "val1", 0) + assert.Equal(t, 1, lc.Len()) + + v, ok := lc.Peek("key1") + assert.Equal(t, v, "val1") + assert.True(t, ok) + + v, ok = lc.Get("key1") + assert.Equal(t, v, "val1") + assert.True(t, ok) + + time.Sleep(time.Millisecond * 10) // wait for entry to expire + assert.Equal(t, 1, lc.Len()) // but not purged + + v, ok = lc.Peek("key1") + assert.Empty(t, v) + assert.False(t, ok) + + v, ok = lc.Get("key1") + assert.Empty(t, v) + assert.False(t, ok) +} + +func TestCacheRemoveOldest(t *testing.T) { + lc := NewCache[string, string]().WithLRU().WithMaxKeys(2) + + lc.Set("key1", "val1", 0) + assert.Equal(t, 1, lc.Len()) + + v, ok := lc.Get("key1") + assert.True(t, ok) + assert.Equal(t, "val1", v) + + assert.Equal(t, []string{"key1"}, lc.Keys()) + assert.Equal(t, 1, lc.Len()) + + lc.Set("key2", "val2", 0) + assert.Equal(t, []string{"key1", "key2"}, lc.Keys()) + assert.Equal(t, 2, lc.Len()) + + lc.RemoveOldest() + + assert.Equal(t, []string{"key2"}, lc.Keys()) + assert.Equal(t, 1, lc.Len()) +} + +func ExampleCache() { + // make cache with short TTL and 3 max keys + cache := NewCache[string, string]().WithMaxKeys(3).WithTTL(time.Millisecond * 10) + + // set value under key1. + // with 0 ttl (last parameter) will use cache-wide setting instead (10ms). + cache.Set("key1", "val1", 0) + + // get value under key1 + r, ok := cache.Get("key1") + + // check for OK value, because otherwise return would be nil and + // type conversion will panic + if ok { + fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r) + } + + time.Sleep(time.Millisecond * 11) + + // get value under key1 after key expiration + r, ok = cache.Get("key1") + // don't convert to string as with ok == false value would be nil + fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r) + + // set value under key2, would evict old entry because it is already expired. + // ttl (last parameter) overrides cache-wide ttl. + cache.Set("key2", "val2", time.Minute*5) + + fmt.Printf("%+v\n", cache) + // Output: + // value before expiration is found: true, value: "val1" + // value after expiration is found: false, value: "" + // Size: 1, Stats: {Hits:1 Misses:1 Added:2 Evicted:1} (50.0%) +} diff --git a/v3/go.mod b/v3/go.mod new file mode 100644 index 0000000..e7d402b --- /dev/null +++ b/v3/go.mod @@ -0,0 +1,11 @@ +module github.com/go-pkgz/expirable-cache/v3 + +go 1.20 + +require 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 +) diff --git a/v3/go.sum b/v3/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/v3/go.sum @@ -0,0 +1,10 @@ +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/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= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/options.go b/v3/options.go new file mode 100644 index 0000000..b5fe65d --- /dev/null +++ b/v3/options.go @@ -0,0 +1,36 @@ +package cache + +import "time" + +type options[K comparable, V any] interface { + WithTTL(ttl time.Duration) Cache[K, V] + WithMaxKeys(maxKeys int) Cache[K, V] + WithLRU() Cache[K, V] + WithOnEvicted(fn func(key K, value V)) Cache[K, V] +} + +// 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 + 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 + return c +} + +// WithLRU sets cache to LRU (Least Recently Used) eviction mode. +func (c *cacheImpl[K, V]) WithLRU() Cache[K, V] { + c.isLRU = true + 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 + return c +} From f92250c0b1aed288833335fd6cc093b4f58ffe30 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Mon, 19 Feb 2024 04:16:23 +0100 Subject: [PATCH 2/3] add v3 compatible with simplelru v2 had most of the required functions, so this change adds missing ones to satisfy the simplelru interface. To do that, RemoveOldest started returning parameters, unlike being void as before. Another behaviour change is that Get and Peek now return the cached value in case it has already expired to be consistent with the `simplelru` implementation. Also, GetExpiration is added. --- README.md | 4 +- v3/cache.go | 121 ++++++++++++++++++++++-- v3/cache_test.go | 234 +++++++++++++++++++++++++++++++++++++++++++++-- v3/go.mod | 1 + v3/go.sum | 2 + 5 files changed, 343 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ebf9371..f7bd4db 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ either using LRC or LRU eviction. run cache.DeleteExpired periodically using [time.Ticker](https://golang.org/pkg/time/#Ticker), advisable period is 1/2 of TTL. -This cache is heavily inspired by [hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) _simplelru_ implementation. Key differences are: +This cache is heavily inspired by [hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) _simplelru_ implementation. v3 implements `simplelru.LRUCache` interface, so if you use a subset of functions, so you can switch from `github.com/hashicorp/golang-lru/v2/simplelru` or `github.com/hashicorp/golang-lru/v2/expirable` without any changes in your code except for cache creation. Key differences are: - Support LRC (Least Recently Created) in addition to LRU and TTL-based eviction - Supports per-key TTL setting @@ -34,7 +34,7 @@ import ( "fmt" "time" - "github.com/go-pkgz/expirable-cache/v2" + "github.com/go-pkgz/expirable-cache/v3" ) func main() { diff --git a/v3/cache.go b/v3/cache.go index b934691..5b4642d 100644 --- a/v3/cache.go +++ b/v3/cache.go @@ -23,16 +23,23 @@ import ( type Cache[K comparable, V any] interface { fmt.Stringer options[K, V] + Add(key K, value V) bool Set(key K, value V, ttl time.Duration) Get(key K) (V, bool) + GetExpiration(key K) (time.Time, bool) + GetOldest() (K, V, bool) + Contains(key K) (ok bool) Peek(key K) (V, bool) + Values() []V Keys() []K Len() int + Remove(key K) bool Invalidate(key K) InvalidateFn(fn func(key K) bool) - RemoveOldest() + RemoveOldest() (K, V, bool) DeleteExpired() Purge() + Resize(int) int Stat() Stats } @@ -71,8 +78,22 @@ func NewCache[K comparable, V any]() Cache[K, V] { } } +// Add adds a value to the cache. Returns true if an eviction occurred. +// Returns false if there was no eviction: the item was already in the cache, +// or the size was not exceeded. +func (c *cacheImpl[K, V]) Add(key K, value V) (evicted bool) { + return c.addWithTTL(key, value, c.ttl) +} + // Set key, ttl of 0 would use cache-wide TTL func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { + c.addWithTTL(key, value, ttl) +} + +// Returns true if an eviction occurred. +// Returns false if there was no eviction: the item was already in the cache, +// or the size was not exceeded. +func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted bool) { c.Lock() defer c.Unlock() now := time.Now() @@ -85,7 +106,7 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { c.evictList.MoveToFront(ent) ent.Value.(*cacheItem[K, V]).value = value ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl) - return + return false } // Add new item @@ -94,15 +115,17 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { c.items[key] = entry c.stat.Added++ - // Remove oldest entry if it is expired, only in case of non-default TTL. + // Remove the oldest entry if it is expired, only in case of non-default TTL. if c.ttl != noEvictionTTL || ttl != noEvictionTTL { c.removeOldestIfExpired() } + evict := c.maxKeys > 0 && len(c.items) > c.maxKeys // Verify size not exceeded - if c.maxKeys > 0 && len(c.items) > c.maxKeys { + if evict { c.removeOldest() } + return evict } // Get returns the key value if it's not expired @@ -114,7 +137,7 @@ func (c *cacheImpl[K, V]) Get(key K) (V, bool) { // Expired item check if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { c.stat.Misses++ - return def, false + return ent.Value.(*cacheItem[K, V]).value, false } if c.isLRU { c.evictList.MoveToFront(ent) @@ -126,6 +149,15 @@ func (c *cacheImpl[K, V]) Get(key K) (V, bool) { return def, false } +// Contains checks if a key is in the cache, without updating the recent-ness +// or deleting it for being stale. +func (c *cacheImpl[K, V]) Contains(key K) (ok bool) { + c.Lock() + defer c.Unlock() + _, ok = c.items[key] + return 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) { @@ -136,7 +168,7 @@ func (c *cacheImpl[K, V]) Peek(key K) (V, bool) { // Expired item check if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { c.stat.Misses++ - return def, false + return ent.Value.(*cacheItem[K, V]).value, false } c.stat.Hits++ return ent.Value.(*cacheItem[K, V]).value, true @@ -145,6 +177,16 @@ func (c *cacheImpl[K, V]) Peek(key K) (V, bool) { return def, false } +// GetExpiration returns the expiration time of the key. Non-existing key returns zero time. +func (c *cacheImpl[K, V]) GetExpiration(key K) (time.Time, bool) { + c.Lock() + defer c.Unlock() + if ent, ok := c.items[key]; ok { + return ent.Value.(*cacheItem[K, V]).expiresAt, true + } + return time.Time{}, false +} + // Keys returns a slice of the keys in the cache, from oldest to newest. func (c *cacheImpl[K, V]) Keys() []K { c.Lock() @@ -152,6 +194,22 @@ func (c *cacheImpl[K, V]) Keys() []K { return c.keys() } +// Values returns a slice of the values in the cache, from oldest to newest. +// Expired entries are filtered out. +func (c *cacheImpl[K, V]) Values() []V { + c.Lock() + defer c.Unlock() + values := make([]V, 0, len(c.items)) + now := time.Now() + for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() { + if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) { + continue + } + values = append(values, ent.Value.(*cacheItem[K, V]).value) + } + return values +} + // Len return count of items in cache, including expired func (c *cacheImpl[K, V]) Len() int { c.Lock() @@ -159,6 +217,25 @@ func (c *cacheImpl[K, V]) Len() int { return c.evictList.Len() } +// Resize changes the cache size. Size of 0 means unlimited. +func (c *cacheImpl[K, V]) Resize(size int) int { + c.Lock() + defer c.Unlock() + if size <= 0 { + c.maxKeys = 0 + return 0 + } + diff := c.evictList.Len() - size + if diff < 0 { + diff = 0 + } + for i := 0; i < diff; i++ { + c.removeOldest() + } + c.maxKeys = size + return diff +} + // Invalidate key (item) from the cache func (c *cacheImpl[K, V]) Invalidate(key K) { c.Lock() @@ -179,11 +256,37 @@ func (c *cacheImpl[K, V]) InvalidateFn(fn func(key K) bool) { } } -// RemoveOldest remove oldest element in the cache -func (c *cacheImpl[K, V]) RemoveOldest() { +// Remove removes the provided key from the cache, returning if the +// key was contained. +func (c *cacheImpl[K, V]) Remove(key K) bool { + c.Lock() + defer c.Unlock() + if ent, ok := c.items[key]; ok { + c.removeElement(ent) + return true + } + return false +} + +// RemoveOldest remove the oldest element in the cache +func (c *cacheImpl[K, V]) RemoveOldest() (key K, value V, ok bool) { c.Lock() defer c.Unlock() - c.removeOldest() + if ent := c.evictList.Back(); ent != nil { + c.removeElement(ent) + return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true + } + return +} + +// GetOldest returns the oldest entry +func (c *cacheImpl[K, V]) GetOldest() (key K, value V, ok bool) { + c.Lock() + defer c.Unlock() + if ent := c.evictList.Back(); ent != nil { + return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true + } + return } // DeleteExpired clears cache of expired items diff --git a/v3/cache_test.go b/v3/cache_test.go index 49b23c6..ea56d48 100644 --- a/v3/cache_test.go +++ b/v3/cache_test.go @@ -1,29 +1,178 @@ package cache import ( + "crypto/rand" "fmt" + "math" + "math/big" + "reflect" "sync" "testing" "time" + "github.com/hashicorp/golang-lru/v2/simplelru" "github.com/stretchr/testify/assert" ) +func getRand(tb testing.TB) int64 { + out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + tb.Fatal(err) + } + return out.Int64() +} + +func BenchmarkLRU_Rand_NoExpire(b *testing.B) { + l := NewCache[int64, int64]().WithLRU().WithMaxKeys(8192) + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = getRand(b) % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func BenchmarkLRU_Freq_NoExpire(b *testing.B) { + l := NewCache[int64, int64]().WithLRU().WithMaxKeys(8192) + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = getRand(b) % 16384 + } else { + trace[i] = getRand(b) % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func BenchmarkLRU_Rand_WithExpire(b *testing.B) { + l := NewCache[int64, int64]().WithLRU().WithMaxKeys(8192).WithTTL(time.Millisecond * 10) + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = getRand(b) % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func BenchmarkLRU_Freq_WithExpire(b *testing.B) { + l := NewCache[int64, int64]().WithLRU().WithMaxKeys(8192).WithTTL(time.Millisecond * 10) + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = getRand(b) % 16384 + } else { + trace[i] = getRand(b) % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func TestSimpleLRUInterface(_ *testing.T) { + var _ simplelru.LRUCache[int, int] = NewCache[int, int]() +} + func TestCacheNoPurge(t *testing.T) { lc := NewCache[string, string]() - lc.Set("key1", "val1", 0) + k, v, ok := lc.GetOldest() + assert.Empty(t, k) + assert.Empty(t, v) + assert.False(t, ok) + + lc.Add("key1", "val1") assert.Equal(t, 1, lc.Len()) + assert.True(t, lc.Contains("key1")) + assert.False(t, lc.Contains("key2")) - v, ok := lc.Peek("key1") + v, ok = lc.Peek("key1") + assert.Equal(t, "val1", v) + assert.True(t, ok) + + k, v, ok = lc.GetOldest() + assert.Equal(t, "key1", k) assert.Equal(t, "val1", v) assert.True(t, ok) + lc.Add("key3", "val3") + lc.Add("key4", "val4") + lc.Peek("key3") + k, v, ok = lc.GetOldest() + assert.Equal(t, "key1", k) + assert.Equal(t, "val1", v) + assert.True(t, ok) + + lc.Add("key1", "val1") + k, v, ok = lc.GetOldest() + assert.Equal(t, "key3", k) + assert.Equal(t, "val3", v) + assert.True(t, ok) + v, ok = lc.Peek("key2") assert.Empty(t, v) assert.False(t, ok) - assert.Equal(t, []string{"key1"}, lc.Keys()) + assert.Equal(t, []string{"key3", "key4", "key1"}, lc.Keys()) } func TestCacheWithDeleteExpired(t *testing.T) { @@ -67,6 +216,24 @@ func TestCacheWithDeleteExpired(t *testing.T) { assert.Equal(t, []string{"key1", "val1", "key2", "val2"}, evicted) } +func TestCache_Values(t *testing.T) { + lc := NewCache[string, string]().WithMaxKeys(3) + + lc.Add("key1", "val1") + lc.Add("key2", "val2") + lc.Add("key3", "val3") + + values := lc.Values() + if !reflect.DeepEqual(values, []string{"val1", "val2", "val3"}) { + t.Fatalf("values differs from expected") + } + + assert.Equal(t, 0, lc.Resize(0)) + assert.Equal(t, 1, lc.Resize(2)) + assert.Equal(t, 0, lc.Resize(5)) + assert.Equal(t, 1, lc.Resize(1)) +} + func TestCacheWithPurgeEnforcedBySize(t *testing.T) { lc := NewCache[string, string]().WithTTL(time.Hour).WithMaxKeys(10) @@ -102,6 +269,7 @@ func TestCacheInvalidateAndEvict(t *testing.T) { lc.Set("key1", "val1", 0) lc.Set("key2", "val2", 0) + lc.Set("key3", "val3", 0) val, ok := lc.Get("key1") assert.True(t, ok) @@ -124,7 +292,15 @@ func TestCacheInvalidateAndEvict(t *testing.T) { assert.Equal(t, 2, evicted) _, ok = lc.Get("key2") assert.False(t, ok) - assert.Equal(t, 0, lc.Len()) + assert.Equal(t, 1, lc.Len()) + + assert.True(t, lc.Remove("key3")) + assert.Equal(t, 3, evicted) + val, ok = lc.Get("key3") + assert.Empty(t, val) + assert.False(t, ok) + assert.False(t, lc.Remove("key3")) + assert.Zero(t, lc.Len()) } func TestCacheExpired(t *testing.T) { @@ -145,12 +321,38 @@ func TestCacheExpired(t *testing.T) { assert.Equal(t, 1, lc.Len()) // but not purged v, ok = lc.Peek("key1") - assert.Empty(t, v) + assert.Equal(t, "val1", v, "expired and marked as such, but value is available") assert.False(t, ok) v, ok = lc.Get("key1") - assert.Empty(t, v) + assert.Equal(t, "val1", v, "expired and marked as such, but value is available") assert.False(t, ok) + + assert.Empty(t, lc.Values()) +} + +func TestCache_GetExpiration(t *testing.T) { + lc := NewCache[string, string]().WithTTL(time.Second * 5) + + lc.Set("key1", "val1", time.Second*5) + assert.Equal(t, 1, lc.Len()) + + exp, ok := lc.GetExpiration("key1") + assert.True(t, ok) + assert.True(t, exp.After(time.Now().Add(time.Second*4))) + assert.True(t, exp.Before(time.Now().Add(time.Second*6))) + + lc.Set("key2", "val2", time.Second*10) + assert.Equal(t, 2, lc.Len()) + + exp, ok = lc.GetExpiration("key2") + assert.True(t, ok) + assert.True(t, exp.After(time.Now().Add(time.Second*9))) + assert.True(t, exp.Before(time.Now().Add(time.Second*11))) + + exp, ok = lc.GetExpiration("non-existing-key") + assert.False(t, ok) + assert.Zero(t, exp) } func TestCacheRemoveOldest(t *testing.T) { @@ -170,10 +372,26 @@ func TestCacheRemoveOldest(t *testing.T) { assert.Equal(t, []string{"key1", "key2"}, lc.Keys()) assert.Equal(t, 2, lc.Len()) - lc.RemoveOldest() + k, v, ok := lc.RemoveOldest() + assert.Equal(t, "key1", k) + assert.Equal(t, "val1", v) + assert.True(t, ok) assert.Equal(t, []string{"key2"}, lc.Keys()) assert.Equal(t, 1, lc.Len()) + + k, v, ok = lc.RemoveOldest() + assert.Equal(t, "key2", k) + assert.Equal(t, "val2", v) + assert.True(t, ok) + + k, v, ok = lc.RemoveOldest() + assert.Empty(t, k) + assert.Empty(t, v) + assert.False(t, ok) + + assert.Empty(t, lc.Keys()) + } func ExampleCache() { @@ -207,6 +425,6 @@ func ExampleCache() { fmt.Printf("%+v\n", cache) // Output: // value before expiration is found: true, value: "val1" - // value after expiration is found: false, value: "" + // value after expiration is found: false, value: "val1" // Size: 1, Stats: {Hits:1 Misses:1 Added:2 Evicted:1} (50.0%) } diff --git a/v3/go.mod b/v3/go.mod index e7d402b..fad54a3 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.8.4 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index fa4b6e6..e22664c 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,5 +1,7 @@ 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/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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= From 00002e0041dd47767f0d1d42b2a8b181d8e7b0e1 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Tue, 20 Feb 2024 17:30:36 +0100 Subject: [PATCH 3/3] migrate v2 to using v3 code instead of duplicating it --- v2/cache.go | 226 ++++++-------------------------------------------- v2/go.mod | 7 +- v2/go.sum | 1 + v2/options.go | 8 +- 4 files changed, 36 insertions(+), 206 deletions(-) diff --git a/v2/cache.go b/v2/cache.go index b934691..9cd12d6 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 behavior 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 behavior 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 }