From a198afa702f7f8e3c9d5ba041770f34aecbec927 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 3 Sep 2022 12:12:24 -0500 Subject: [PATCH] Support generics (#3) * quick POC for generic implementation * disable non-1.18 compat linters * fix Example * embed options into Cache interface Co-authored-by: Dmitry Verkhoturov --- .github/workflows/ci-v2.yml | 48 +++++++ .github/workflows/ci.yml | 14 +- .golangci.yml | 10 +- README.md | 4 +- v2/.golangci.yml | 64 +++++++++ v2/cache.go | 268 ++++++++++++++++++++++++++++++++++++ v2/cache_test.go | 212 ++++++++++++++++++++++++++++ v2/go.mod | 11 ++ v2/go.sum | 11 ++ v2/options.go | 36 +++++ 10 files changed, 664 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/ci-v2.yml create mode 100644 v2/.golangci.yml create mode 100644 v2/cache.go create mode 100644 v2/cache_test.go create mode 100644 v2/go.mod create mode 100644 v2/go.sum create mode 100644 v2/options.go diff --git a/.github/workflows/ci-v2.yml b/.github/workflows/ci-v2.yml new file mode 100644 index 0000000..12a5324 --- /dev/null +++ b/.github/workflows/ci-v2.yml @@ -0,0 +1,48 @@ +name: build-v2 + +on: + push: + branches: + tags: + paths: + - ".github/workflows/ci-v2.yml" + - "v2/**" + pull_request: + paths: + - ".github/workflows/ci-v2.yml" + - "v2/**" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: set up go 1.18 + uses: actions/setup-go@v2 + with: + go-version: 1.18 + id: go + + - name: checkout + uses: actions/checkout@v2 + + - name: build and test + run: | + go test -timeout=60s -race -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov + go build -race + working-directory: v2 + + - name: install golangci-lint and goveralls + run: | + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.46.2 + GO111MODULE=off go get -u -v github.com/mattn/goveralls + + - name: run linters + run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions + working-directory: v2 + + - name: submit coverage + run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: v2 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 517459c..0f12846 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,17 +4,23 @@ on: push: branches: tags: + paths-ignore: + - ".github/workflows/ci-v2.yml" + - "v2/**" pull_request: + paths-ignore: + - ".github/workflows/ci-v2.yml" + - "v2/**" jobs: build: runs-on: ubuntu-latest steps: - - name: set up go 1.14 - uses: actions/setup-go@v1 + - name: set up go 1.18 + uses: actions/setup-go@v2 with: - go-version: 1.14 + go-version: 1.18 id: go - name: checkout @@ -27,7 +33,7 @@ jobs: - name: install golangci-lint and goveralls run: | - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.25.0 + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.46.2 GO111MODULE=off go get -u -v github.com/mattn/goveralls - name: run linters diff --git a/.golangci.yml b/.golangci.yml index 7442cd4..870d3a5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,6 @@ linters-settings: govet: check-shadowing: true - golint: - min-confidence: 0 gocyclo: min-complexity: 15 maligned: @@ -25,7 +23,7 @@ linters-settings: linters: enable: - megacheck - - golint + - revive - govet - unconvert - megacheck @@ -42,7 +40,7 @@ linters: - varcheck - stylecheck - gochecknoinits - - scopelint + - exportloopref - gocritic - nakedret - gosimple @@ -57,8 +55,4 @@ run: - vendor issues: - exclude-rules: - - text: "should have a package comment, unless it's in another file for this package" - linters: - - golint exclude-use-default: false diff --git a/README.md b/README.md index e0ab7b7..536e586 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,12 @@ import ( "fmt" "time" - "github.com/go-pkgz/expirable-cache" + "github.com/go-pkgz/expirable-cache/v2" ) func main() { // make cache with short TTL and 3 max keys - c, _ := cache.NewCache(cache.MaxKeys(3), cache.TTL(time.Millisecond*10)) + c := 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). diff --git a/v2/.golangci.yml b/v2/.golangci.yml new file mode 100644 index 0000000..492fc22 --- /dev/null +++ b/v2/.golangci.yml @@ -0,0 +1,64 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + maligned: + suggest-new: true + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + +linters: + enable: + - megacheck + - revive + - govet + - unconvert + - megacheck + #- structcheck + - gas + - gocyclo + - dupl + - misspell + #- unparam + - varcheck + - deadcode + - typecheck + - ineffassign + - varcheck + - stylecheck + - gochecknoinits + - exportloopref + #- gocritic + - nakedret + - gosimple + - prealloc + fast: false + disable-all: true + +run: + output: + format: tab + skip-dirs: + - vendor + +issues: + exclude-rules: + - text: "should have a package comment, unless it's in another file for this package" + linters: + - golint + exclude-use-default: false diff --git a/v2/cache.go b/v2/cache.go new file mode 100644 index 0000000..b934691 --- /dev/null +++ b/v2/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/v2/cache_test.go b/v2/cache_test.go new file mode 100644 index 0000000..49b23c6 --- /dev/null +++ b/v2/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/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..0fda1c4 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,11 @@ +module github.com/go-pkgz/expirable-cache/v2 + +go 1.18 + +require github.com/stretchr/testify v1.7.1 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..2dca7c9 --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,11 @@ +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= +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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/options.go b/v2/options.go new file mode 100644 index 0000000..b5fe65d --- /dev/null +++ b/v2/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 +}