From 2d92605b707dad3239408f5394f6e25578899bd1 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 22 Oct 2024 08:47:13 +0200 Subject: [PATCH 01/35] new option to define the max memory the cache can use in bytes --- options.go | 9 +++++++++ options_test.go | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/options.go b/options.go index 8a6088c..4a739e2 100644 --- a/options.go +++ b/options.go @@ -18,6 +18,7 @@ func (fn optionFunc[K, V]) apply(opts *options[K, V]) { // options holds all available cache configuration options. type options[K comparable, V any] struct { capacity uint64 + sizeInBytes uint64 ttl time.Duration loader Loader[K, V] disableTouchOnHit bool @@ -75,3 +76,11 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { opts.disableTouchOnHit = true }) } + +// WithMemorySize sets the maximum memory size the cache is allowed to grow. +// If used together with WithCapacity, WithMemorySize overrules the maximum capacity. +func WithMemorySize[K comparable, V any](s uint64) Option[K, V] { + return optionFunc[K, V](func(opts *options[K, V]) { + opts.sizeInBytes = s + }) +} diff --git a/options_test.go b/options_test.go index 8cf0fb2..e248f64 100644 --- a/options_test.go +++ b/options_test.go @@ -68,3 +68,10 @@ func Test_WithDisableTouchOnHit(t *testing.T) { WithDisableTouchOnHit[string, string]().apply(&opts) assert.True(t, opts.disableTouchOnHit) } + +func Test_WithMemorySize(t *testing.T) { + var opts options[string, string] + + WithMemorySize[string, string](1024).apply(&opts) + assert.Equal(t, uint64(1024), opts.sizeInBytes) +} From 6e92deb3b7e755ab332e11670c82b0290cab626f Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 22 Oct 2024 08:48:19 +0200 Subject: [PATCH 02/35] cache.set updated to cope with memory limitations --- cache.go | 38 ++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++++ 3 files changed, 43 insertions(+) diff --git a/cache.go b/cache.go index fbfaebe..504daef 100644 --- a/cache.go +++ b/cache.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/DmitriyVTitov/size" "golang.org/x/sync/singleflight" ) @@ -15,6 +16,7 @@ const ( EvictionReasonDeleted EvictionReason = iota + 1 EvictionReasonCapacityReached EvictionReasonExpired + EvictionReasonMaxMemorySizeExceeded ) // EvictionReason is used to specify why a certain item was @@ -36,6 +38,7 @@ type Cache[K comparable, V any] struct { timerCh chan time.Duration } + sizeInBytes uint64 metricsMu sync.RWMutex metrics Metrics @@ -137,7 +140,23 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { if elem != nil { // update/overwrite an existing item item := elem.Value.(*Item[K, V]) + oldValue := item.value item.update(value, ttl) + + if c.options.sizeInBytes != 0 { + oldSize := size.Of(oldValue) + newSize := size.Of(value) + + // size.Of returns -1 on errors + if oldSize != -1 && newSize != -1 { + c.sizeInBytes = c.sizeInBytes - uint64(oldSize) + uint64(newSize) + } + + for c.sizeInBytes > c.options.sizeInBytes { + c.evict(EvictionReasonMaxMemorySizeExceeded, c.items.lru.Back()) + } + } + c.updateExpirations(false, elem) return item @@ -158,6 +177,17 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.items.values[key] = elem c.updateExpirations(true, elem) + if c.options.sizeInBytes != 0 { + itemSize := size.Of(item) + if itemSize != -1 { + c.sizeInBytes += uint64(itemSize) + } + + for c.sizeInBytes > c.options.sizeInBytes { + c.evict(EvictionReasonMaxMemorySizeExceeded, c.items.lru.Back()) + } + } + c.metricsMu.Lock() c.metrics.Insertions++ c.metricsMu.Unlock() @@ -258,6 +288,14 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { for i := range elems { item := elems[i].Value.(*Item[K, V]) delete(c.items.values, item.key) + + if c.options.sizeInBytes != 0 { + itemSize := size.Of(item) + if itemSize != -1 { + c.sizeInBytes -= uint64(itemSize) + } + } + c.items.lru.Remove(elems[i]) c.items.expQueue.remove(elems[i]) diff --git a/go.mod b/go.mod index 0ddba4a..1aa52ce 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/jellydator/ttlcache/v3 go 1.18 require ( + github.com/DmitriyVTitov/size v1.5.0 github.com/stretchr/testify v1.9.0 go.uber.org/goleak v1.3.0 golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum index f014f5f..83cc0bf 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ +github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= +github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= From 666a9876c2bee835855d44b09abbfc42d059f8e7 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 16:40:33 +0100 Subject: [PATCH 03/35] options updated to implement the suggested new option --- options.go | 18 +++++++++++++----- options_test.go | 11 ++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/options.go b/options.go index 4a739e2..0f34edc 100644 --- a/options.go +++ b/options.go @@ -15,10 +15,15 @@ func (fn optionFunc[K, V]) apply(opts *options[K, V]) { fn(opts) } +// CostCalcFunc is used to calculate the costs of the key and the item to be +// inserted into the cache. +type CostCalcFunc[K comparable, V any] func(key K, item V) uint64 + // options holds all available cache configuration options. type options[K comparable, V any] struct { capacity uint64 - sizeInBytes uint64 + totalCost uint64 + costsCalFunc CostCalcFunc[K, V] ttl time.Duration loader Loader[K, V] disableTouchOnHit bool @@ -77,10 +82,13 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { }) } -// WithMemorySize sets the maximum memory size the cache is allowed to grow. -// If used together with WithCapacity, WithMemorySize overrules the maximum capacity. -func WithMemorySize[K comparable, V any](s uint64) Option[K, V] { +// WithTotalCost sets the maximum costs the cache is allowed to use (e.g. the used memory). +// The actual costs calculation for each inserted item happens by making use of the +// callback CostCalcFunc. +// If used together with WithCapacity, WithTotalCost overrules the maximum capacity. +func WithTotalCost[K comparable, V any](s uint64, callback CostCalcFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { - opts.sizeInBytes = s + opts.totalCost = s + opts.costsCalFunc = callback }) } diff --git a/options_test.go b/options_test.go index e248f64..16841f5 100644 --- a/options_test.go +++ b/options_test.go @@ -1,10 +1,9 @@ package ttlcache import ( + "github.com/stretchr/testify/assert" "testing" "time" - - "github.com/stretchr/testify/assert" ) func Test_optionFunc_apply(t *testing.T) { @@ -69,9 +68,11 @@ func Test_WithDisableTouchOnHit(t *testing.T) { assert.True(t, opts.disableTouchOnHit) } -func Test_WithMemorySize(t *testing.T) { +func Test_WithTotalCost(t *testing.T) { var opts options[string, string] - WithMemorySize[string, string](1024).apply(&opts) - assert.Equal(t, uint64(1024), opts.sizeInBytes) + WithTotalCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) + + assert.Equal(t, uint64(1024), opts.totalCost) + assert.Equal(t, uint64(1), opts.costsCalFunc("test", "foo")) } From 0a2236a491f843b65a1bf142382f374cb5732a3a Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 16:41:01 +0100 Subject: [PATCH 04/35] cache impl updated + tests --- cache.go | 38 +++++++----------- cache_test.go | 108 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 89 insertions(+), 57 deletions(-) diff --git a/cache.go b/cache.go index 504daef..da91af9 100644 --- a/cache.go +++ b/cache.go @@ -7,7 +7,6 @@ import ( "sync" "time" - "github.com/DmitriyVTitov/size" "golang.org/x/sync/singleflight" ) @@ -16,7 +15,7 @@ const ( EvictionReasonDeleted EvictionReason = iota + 1 EvictionReasonCapacityReached EvictionReasonExpired - EvictionReasonMaxMemorySizeExceeded + EvictionReasonTotalCostExceeded ) // EvictionReason is used to specify why a certain item was @@ -38,7 +37,7 @@ type Cache[K comparable, V any] struct { timerCh chan time.Duration } - sizeInBytes uint64 + costs uint64 metricsMu sync.RWMutex metrics Metrics @@ -143,17 +142,14 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { oldValue := item.value item.update(value, ttl) - if c.options.sizeInBytes != 0 { - oldSize := size.Of(oldValue) - newSize := size.Of(value) + if c.options.totalCost != 0 { + oldItemCosts := c.options.costsCalFunc(key, oldValue) + newItemCosts := c.options.costsCalFunc(key, value) - // size.Of returns -1 on errors - if oldSize != -1 && newSize != -1 { - c.sizeInBytes = c.sizeInBytes - uint64(oldSize) + uint64(newSize) - } + c.costs = c.costs - oldItemCosts + newItemCosts - for c.sizeInBytes > c.options.sizeInBytes { - c.evict(EvictionReasonMaxMemorySizeExceeded, c.items.lru.Back()) + for c.costs > c.options.totalCost { + c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -177,14 +173,11 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.items.values[key] = elem c.updateExpirations(true, elem) - if c.options.sizeInBytes != 0 { - itemSize := size.Of(item) - if itemSize != -1 { - c.sizeInBytes += uint64(itemSize) - } + if c.options.totalCost != 0 { + c.costs += c.options.costsCalFunc(key, value) - for c.sizeInBytes > c.options.sizeInBytes { - c.evict(EvictionReasonMaxMemorySizeExceeded, c.items.lru.Back()) + for c.costs > c.options.totalCost { + c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -289,11 +282,8 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { item := elems[i].Value.(*Item[K, V]) delete(c.items.values, item.key) - if c.options.sizeInBytes != 0 { - itemSize := size.Of(item) - if itemSize != -1 { - c.sizeInBytes -= uint64(itemSize) - } + if c.options.totalCost != 0 { + c.costs -= c.options.costsCalFunc(item.key, item.value) } c.items.lru.Remove(elems[i]) diff --git a/cache_test.go b/cache_test.go index 68bd340..3039516 100644 --- a/cache_test.go +++ b/cache_test.go @@ -4,14 +4,13 @@ import ( "container/list" "context" "fmt" - "sync" - "testing" - "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" "golang.org/x/sync/singleflight" + "sync" + "testing" + "time" ) func TestMain(m *testing.M) { @@ -122,7 +121,7 @@ func Test_Cache_updateExpirations(t *testing.T) { t.Run(cn, func(t *testing.T) { t.Parallel() - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) if c.TimerChValue > 0 { cache.items.timerCh <- c.TimerChValue @@ -172,6 +171,7 @@ func Test_Cache_set(t *testing.T) { cc := map[string]struct { Capacity uint64 + MaxCost uint64 Key string TTL time.Duration Metrics Metrics @@ -244,6 +244,33 @@ func Test_Cache_set(t *testing.T) { }, ExpectFns: true, }, + "Set with existing key and eviction caused by exhausted cost": { + MaxCost: 300, + Key: existingKey, + TTL: DefaultTTL, + Metrics: Metrics{ + Insertions: 0, + Evictions: 1, + }, + }, + "Set with existing key and no eviction": { + MaxCost: 500, + Key: existingKey, + TTL: DefaultTTL, + Metrics: Metrics{ + Insertions: 0, + Evictions: 0, + }, + }, + "Set with new key and eviction caused by exhausted cost": { + MaxCost: 400, + Key: newKey, + TTL: DefaultTTL, + Metrics: Metrics{ + Insertions: 1, + Evictions: 1, + }, + }, } for cn, c := range cc { @@ -260,7 +287,7 @@ func Test_Cache_set(t *testing.T) { // calculated based on how addToCache sets ttl existingKeyTTL := time.Hour + time.Minute - cache := prepCache(time.Hour, evictedKey, existingKey, "test3") + cache := prepCache(c.MaxCost, time.Hour, evictedKey, existingKey, "test3") cache.options.capacity = c.Capacity cache.options.ttl = time.Minute * 20 cache.events.insertion.fns[1] = func(item *Item[string, string]) { @@ -269,16 +296,18 @@ func Test_Cache_set(t *testing.T) { } cache.events.insertion.fns[2] = cache.events.insertion.fns[1] cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { - assert.Equal(t, EvictionReasonCapacityReached, r) + if c.MaxCost != 0 { + assert.Equal(t, EvictionReasonTotalCostExceeded, r) + } else { + assert.Equal(t, EvictionReasonCapacityReached, r) + } + assert.Equal(t, evictedKey, item.key) evictionFnsCalls++ } cache.events.eviction.fns[2] = cache.events.eviction.fns[1] - total := 3 - if c.Key == newKey && (c.Capacity == 0 || c.Capacity >= 4) { - total++ - } + total := 3 - int(c.Metrics.Evictions) + int(c.Metrics.Insertions) item := cache.set(c.Key, "value123", c.TTL) @@ -390,7 +419,7 @@ func Test_Cache_get(t *testing.T) { t.Run(cn, func(t *testing.T) { t.Parallel() - cache := prepCache(time.Hour, existingKey, "test2", "test3") + cache := prepCache(0, time.Hour, existingKey, "test2", "test3") addToCache(cache, time.Nanosecond, expiredKey) time.Sleep(time.Millisecond) // force expiration @@ -441,7 +470,7 @@ func Test_Cache_evict(t *testing.T) { key4FnsCalls int ) - cache := prepCache(time.Hour, "1", "2", "3", "4") + cache := prepCache(0, time.Hour, "1", "2", "3", "4") cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { assert.Equal(t, EvictionReasonDeleted, r) switch item.key { @@ -486,7 +515,7 @@ func Test_Cache_evict(t *testing.T) { } func Test_Cache_Set(t *testing.T) { - cache := prepCache(time.Hour, "test1", "test2", "test3") + cache := prepCache(0, time.Hour, "test1", "test2", "test3") item := cache.Set("hello", "value123", time.Minute) require.NotNil(t, item) assert.Same(t, item, cache.items.values["hello"].Value) @@ -599,7 +628,7 @@ func Test_Cache_Get(t *testing.T) { t.Run(cn, func(t *testing.T) { t.Parallel() - cache := prepCache(time.Minute, foundKey, "test2", "test3") + cache := prepCache(0, time.Minute, foundKey, "test2", "test3") oldExpiresAt := cache.items.values[foundKey].Value.(*Item[string, string]).expiresAt cache.options = c.DefaultOptions @@ -632,7 +661,7 @@ func Test_Cache_Get(t *testing.T) { func Test_Cache_Delete(t *testing.T) { var fnsCalls int - cache := prepCache(time.Hour, "1", "2", "3", "4") + cache := prepCache(0, time.Hour, "1", "2", "3", "4") cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { assert.Equal(t, EvictionReasonDeleted, r) fnsCalls++ @@ -652,7 +681,7 @@ func Test_Cache_Delete(t *testing.T) { } func Test_Cache_Has(t *testing.T) { - cache := prepCache(time.Hour, "1") + cache := prepCache(0, time.Hour, "1") addToCache(cache, time.Nanosecond, "2") assert.True(t, cache.Has("1")) @@ -661,7 +690,7 @@ func Test_Cache_Has(t *testing.T) { } func Test_Cache_GetOrSet(t *testing.T) { - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) item, retrieved := cache.GetOrSet("test", "1", WithTTL[string, string](time.Minute)) require.NotNil(t, item) assert.Same(t, item, cache.items.values["test"].Value) @@ -685,7 +714,7 @@ func Test_Cache_GetOrSet(t *testing.T) { } func Test_Cache_GetAndDelete(t *testing.T) { - cache := prepCache(time.Hour, "test1", "test2", "test3") + cache := prepCache(0, time.Hour, "test1", "test2", "test3") listItem := cache.items.lru.Front() require.NotNil(t, listItem) assert.Same(t, listItem, cache.items.values["test3"]) @@ -721,7 +750,7 @@ func Test_Cache_DeleteAll(t *testing.T) { key4FnsCalls int ) - cache := prepCache(time.Hour, "1", "2", "3", "4") + cache := prepCache(0, time.Hour, "1", "2", "3", "4") cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { assert.Equal(t, EvictionReasonDeleted, r) switch item.key { @@ -751,7 +780,7 @@ func Test_Cache_DeleteExpired(t *testing.T) { key2FnsCalls int ) - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { assert.Equal(t, EvictionReasonExpired, r) switch item.key { @@ -792,7 +821,7 @@ func Test_Cache_DeleteExpired(t *testing.T) { } func Test_Cache_Touch(t *testing.T) { - cache := prepCache(time.Hour, "1", "2") + cache := prepCache(0, time.Hour, "1", "2") oldExpiresAt := cache.items.values["1"].Value.(*Item[string, string]).expiresAt cache.Touch("1") @@ -803,7 +832,7 @@ func Test_Cache_Touch(t *testing.T) { } func Test_Cache_Len(t *testing.T) { - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) assert.Equal(t, 0, cache.Len()) addToCache(cache, time.Hour, "1") @@ -820,13 +849,13 @@ func Test_Cache_Len(t *testing.T) { } func Test_Cache_Keys(t *testing.T) { - cache := prepCache(time.Hour, "1", "2", "3") + cache := prepCache(0, time.Hour, "1", "2", "3") addToCache(cache, time.Nanosecond, "4") assert.ElementsMatch(t, []string{"1", "2", "3"}, cache.Keys()) } func Test_Cache_Items(t *testing.T) { - cache := prepCache(time.Hour, "1", "2", "3") + cache := prepCache(0, time.Hour, "1", "2", "3") addToCache(cache, time.Nanosecond, "4") items := cache.Items() require.Len(t, items, 3) @@ -840,7 +869,7 @@ func Test_Cache_Items(t *testing.T) { } func Test_Cache_Range(t *testing.T) { - c := prepCache(DefaultTTL, "1", "2", "3", "4", "5") + c := prepCache(0, DefaultTTL, "1", "2", "3", "4", "5") addToCache(c, time.Nanosecond, "6") var results []string @@ -860,7 +889,7 @@ func Test_Cache_Range(t *testing.T) { } func Test_Cache_RangeBackwards(t *testing.T) { - c := prepCache(DefaultTTL) + c := prepCache(0, DefaultTTL) addToCache(c, time.Nanosecond, "1") addToCache(c, time.Hour, "2", "3", "4", "5") @@ -890,7 +919,7 @@ func Test_Cache_Metrics(t *testing.T) { } func Test_Cache_Start(t *testing.T) { - cache := prepCache(0) + cache := prepCache(0, 0) cache.stopCh = make(chan struct{}) addToCache(cache, time.Nanosecond, "1") @@ -938,7 +967,7 @@ func Test_Cache_Stop(t *testing.T) { func Test_Cache_OnInsertion(t *testing.T) { checkCh := make(chan struct{}) resCh := make(chan struct{}) - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) del1 := cache.OnInsertion(func(_ context.Context, _ *Item[string, string]) { checkCh <- struct{}{} }) @@ -1022,7 +1051,7 @@ func Test_Cache_OnInsertion(t *testing.T) { func Test_Cache_OnEviction(t *testing.T) { checkCh := make(chan struct{}) resCh := make(chan struct{}) - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) del1 := cache.OnEviction(func(_ context.Context, _ EvictionReason, _ *Item[string, string]) { checkCh <- struct{}{} }) @@ -1181,7 +1210,7 @@ func Test_SuppressedLoader_Load(t *testing.T) { item1, item2 *Item[string, string] ) - cache := prepCache(time.Hour) + cache := prepCache(0, time.Hour) // nil result wg.Add(2) @@ -1228,9 +1257,17 @@ func Test_SuppressedLoader_Load(t *testing.T) { assert.Equal(t, 1, loadCalls) } -func prepCache(ttl time.Duration, keys ...string) *Cache[string, string] { +func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, string] { c := &Cache[string, string]{} c.options.ttl = ttl + if maxCost != 0 { + c.options.totalCost = maxCost + c.options.costsCalFunc = func(key string, item string) uint64 { + // 72 bytes are used by the Item struct + // 2 * 16 bytes are used by the used string headers (key and item) + return uint64(104 + len(key) + len(item)) + } + } c.items.values = make(map[string]*list.Element) c.items.lru = list.New() c.items.expQueue = newExpirationQueue[string, string]() @@ -1245,14 +1282,19 @@ func prepCache(ttl time.Duration, keys ...string) *Cache[string, string] { func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { for i, key := range keys { + value := fmt.Sprint("value of", key) item := NewItem( key, - fmt.Sprint("value of", key), + value, ttl+time.Duration(i)*time.Minute, false, ) elem := c.items.lru.PushFront(item) c.items.values[key] = elem c.items.expQueue.push(elem) + + if c.options.totalCost != 0 { + c.costs += c.options.costsCalFunc(key, value) + } } } From fa705e78255b10ac6b808da907983263886e1822 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 16:41:27 +0100 Subject: [PATCH 05/35] dependencies updated --- go.mod | 1 - go.sum | 4 ---- 2 files changed, 5 deletions(-) diff --git a/go.mod b/go.mod index 1aa52ce..0ddba4a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/jellydator/ttlcache/v3 go 1.18 require ( - github.com/DmitriyVTitov/size v1.5.0 github.com/stretchr/testify v1.9.0 go.uber.org/goleak v1.3.0 golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum index 83cc0bf..f014f5f 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,6 @@ -github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= -github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= From dcc974361efdcc7dd29d3fe89f5bff4d6d132dca Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 19:10:25 +0100 Subject: [PATCH 06/35] imports organized --- cache_test.go | 7 ++++--- options_test.go | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cache_test.go b/cache_test.go index 3039516..15c612c 100644 --- a/cache_test.go +++ b/cache_test.go @@ -4,13 +4,14 @@ import ( "container/list" "context" "fmt" + "sync" + "testing" + "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" "golang.org/x/sync/singleflight" - "sync" - "testing" - "time" ) func TestMain(m *testing.M) { diff --git a/options_test.go b/options_test.go index 16841f5..196c74d 100644 --- a/options_test.go +++ b/options_test.go @@ -1,9 +1,10 @@ package ttlcache import ( - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/stretchr/testify/assert" ) func Test_optionFunc_apply(t *testing.T) { From 7d51be1b3236f6a1f41b7fe33f3b85f2659cdedf Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 19:32:39 +0100 Subject: [PATCH 07/35] readme updated to document the new configuration option --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a17cb24..de95407 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ func main() { Note that by default, a new cache instance does not let any of its items to expire or be automatically deleted. However, this feature -can be activated by passing a few additional options into the `ttlcache.New()` function and calling the `cache.Start()` method: ```go func main() { @@ -141,3 +140,24 @@ func main() { item := cache.Get("key from file") } ``` + +To restrict the cache's capacity based on criteria beyond the number +of items it can hold, the `ttlcache.WithTotalCost` option allows for +implementing custom strategies. The following example demonstrates +how to limit the maximum memory usage of a cache to 5MB: +```go +func main() { + cache := ttlcache.New[string, string]( + ttlcache.WithTotalCost[string, string](5120, func(key string, item string) uint64 { + // 72 (bytes) represent the memory occupied by the internal structure + // used to store the new value. + // 16 (bytes) represent the memory footprint of a string header in Go, + // as determined by unsafe.Sizeof. This includes the metadata for the string, + // such as its length and a pointer to the underlying byte array. + return 72 + 16 + len(key) + 16 + len(item) + }), + ) + + item := cache.Get("key from file") +} +``` From fea314a708804df24cfbd270d2e47bedcc19215f Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 19:37:34 +0100 Subject: [PATCH 08/35] better explanation in the example --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index de95407..b08cfed 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ how to limit the maximum memory usage of a cache to 5MB: func main() { cache := ttlcache.New[string, string]( ttlcache.WithTotalCost[string, string](5120, func(key string, item string) uint64 { - // 72 (bytes) represent the memory occupied by the internal structure + // 72 (bytes) represent the memory occupied by the *ttlcache.Item structure // used to store the new value. // 16 (bytes) represent the memory footprint of a string header in Go, // as determined by unsafe.Sizeof. This includes the metadata for the string, @@ -157,7 +157,7 @@ func main() { return 72 + 16 + len(key) + 16 + len(item) }), ) - - item := cache.Get("key from file") + + cache.Set("first", "value1", ttlcache.DefaultTTL) } ``` From e7e24cc538dd424b80d5b126d2e08eb42cae39c0 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 19:56:16 +0100 Subject: [PATCH 09/35] readme line removed by accident restored --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b08cfed..774e40f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ func main() { Note that by default, a new cache instance does not let any of its items to expire or be automatically deleted. However, this feature +can be activated by passing a few additional options into the `ttlcache.New()` function and calling the `cache.Start()` method: ```go func main() { From 53b1934a353a891c0f16ac74423a780ac5290159 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 19:58:20 +0100 Subject: [PATCH 10/35] function renamed (typo fixed) --- cache.go | 8 ++++---- cache_test.go | 4 ++-- options.go | 4 ++-- options_test.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index da91af9..9b269c4 100644 --- a/cache.go +++ b/cache.go @@ -143,8 +143,8 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { item.update(value, ttl) if c.options.totalCost != 0 { - oldItemCosts := c.options.costsCalFunc(key, oldValue) - newItemCosts := c.options.costsCalFunc(key, value) + oldItemCosts := c.options.costsCalcFunc(key, oldValue) + newItemCosts := c.options.costsCalcFunc(key, value) c.costs = c.costs - oldItemCosts + newItemCosts @@ -174,7 +174,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.updateExpirations(true, elem) if c.options.totalCost != 0 { - c.costs += c.options.costsCalFunc(key, value) + c.costs += c.options.costsCalcFunc(key, value) for c.costs > c.options.totalCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) @@ -283,7 +283,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { delete(c.items.values, item.key) if c.options.totalCost != 0 { - c.costs -= c.options.costsCalFunc(item.key, item.value) + c.costs -= c.options.costsCalcFunc(item.key, item.value) } c.items.lru.Remove(elems[i]) diff --git a/cache_test.go b/cache_test.go index 15c612c..31a7228 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1263,7 +1263,7 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c.options.ttl = ttl if maxCost != 0 { c.options.totalCost = maxCost - c.options.costsCalFunc = func(key string, item string) uint64 { + c.options.costsCalcFunc = func(key string, item string) uint64 { // 72 bytes are used by the Item struct // 2 * 16 bytes are used by the used string headers (key and item) return uint64(104 + len(key) + len(item)) @@ -1295,7 +1295,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.expQueue.push(elem) if c.options.totalCost != 0 { - c.costs += c.options.costsCalFunc(key, value) + c.costs += c.options.costsCalcFunc(key, value) } } } diff --git a/options.go b/options.go index 0f34edc..4c643f7 100644 --- a/options.go +++ b/options.go @@ -23,7 +23,7 @@ type CostCalcFunc[K comparable, V any] func(key K, item V) uint64 type options[K comparable, V any] struct { capacity uint64 totalCost uint64 - costsCalFunc CostCalcFunc[K, V] + costsCalcFunc CostCalcFunc[K, V] ttl time.Duration loader Loader[K, V] disableTouchOnHit bool @@ -89,6 +89,6 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { func WithTotalCost[K comparable, V any](s uint64, callback CostCalcFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.totalCost = s - opts.costsCalFunc = callback + opts.costsCalcFunc = callback }) } diff --git a/options_test.go b/options_test.go index 196c74d..016310a 100644 --- a/options_test.go +++ b/options_test.go @@ -75,5 +75,5 @@ func Test_WithTotalCost(t *testing.T) { WithTotalCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) assert.Equal(t, uint64(1024), opts.totalCost) - assert.Equal(t, uint64(1), opts.costsCalFunc("test", "foo")) + assert.Equal(t, uint64(1), opts.costsCalcFunc("test", "foo")) } From 7a9baa3de68db3ede245b4da4fa5ffd16e3a741d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 20:00:58 +0100 Subject: [PATCH 11/35] wording fixed --- options.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/options.go b/options.go index 4c643f7..0ee1354 100644 --- a/options.go +++ b/options.go @@ -15,7 +15,7 @@ func (fn optionFunc[K, V]) apply(opts *options[K, V]) { fn(opts) } -// CostCalcFunc is used to calculate the costs of the key and the item to be +// CostCalcFunc is used to calculate the cost of the key and the item to be // inserted into the cache. type CostCalcFunc[K comparable, V any] func(key K, item V) uint64 @@ -82,8 +82,8 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { }) } -// WithTotalCost sets the maximum costs the cache is allowed to use (e.g. the used memory). -// The actual costs calculation for each inserted item happens by making use of the +// WithTotalCost sets the maximum cost the cache is allowed to use (e.g. the used memory). +// The actual cost calculation for each inserted item happens by making use of the // callback CostCalcFunc. // If used together with WithCapacity, WithTotalCost overrules the maximum capacity. func WithTotalCost[K comparable, V any](s uint64, callback CostCalcFunc[K, V]) Option[K, V] { From 0b175920942fccf81a8350edc4171198cba2a117 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 10 Nov 2024 20:01:13 +0100 Subject: [PATCH 12/35] useless sentence removed --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 774e40f..9249ca2 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,7 @@ func main() { // 72 (bytes) represent the memory occupied by the *ttlcache.Item structure // used to store the new value. // 16 (bytes) represent the memory footprint of a string header in Go, - // as determined by unsafe.Sizeof. This includes the metadata for the string, - // such as its length and a pointer to the underlying byte array. + // as determined by unsafe.Sizeof. return 72 + 16 + len(key) + 16 + len(item) }), ) From d1bdbee786dbd278e13309d7cf76f3fcf1142fc2 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:52:31 +0100 Subject: [PATCH 13/35] costs renamed to cost --- cache.go | 12 ++++++------ cache_test.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cache.go b/cache.go index 9b269c4..1ad16ad 100644 --- a/cache.go +++ b/cache.go @@ -37,7 +37,7 @@ type Cache[K comparable, V any] struct { timerCh chan time.Duration } - costs uint64 + cost uint64 metricsMu sync.RWMutex metrics Metrics @@ -146,9 +146,9 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { oldItemCosts := c.options.costsCalcFunc(key, oldValue) newItemCosts := c.options.costsCalcFunc(key, value) - c.costs = c.costs - oldItemCosts + newItemCosts + c.cost = c.cost - oldItemCosts + newItemCosts - for c.costs > c.options.totalCost { + for c.cost > c.options.totalCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -174,9 +174,9 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.updateExpirations(true, elem) if c.options.totalCost != 0 { - c.costs += c.options.costsCalcFunc(key, value) + c.cost += c.options.costsCalcFunc(key, value) - for c.costs > c.options.totalCost { + for c.cost > c.options.totalCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -283,7 +283,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { delete(c.items.values, item.key) if c.options.totalCost != 0 { - c.costs -= c.options.costsCalcFunc(item.key, item.value) + c.cost -= c.options.costsCalcFunc(item.key, item.value) } c.items.lru.Remove(elems[i]) diff --git a/cache_test.go b/cache_test.go index 31a7228..c7c910c 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1295,7 +1295,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.expQueue.push(elem) if c.options.totalCost != 0 { - c.costs += c.options.costsCalcFunc(key, value) + c.cost += c.options.costsCalcFunc(key, value) } } } From a2f59151fc04ff9265d3111945b361142f5e576c Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:55:25 +0100 Subject: [PATCH 14/35] cost used in tests simplified --- cache_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cache_test.go b/cache_test.go index c7c910c..b1a2e4b 100644 --- a/cache_test.go +++ b/cache_test.go @@ -246,7 +246,7 @@ func Test_Cache_set(t *testing.T) { ExpectFns: true, }, "Set with existing key and eviction caused by exhausted cost": { - MaxCost: 300, + MaxCost: 30, Key: existingKey, TTL: DefaultTTL, Metrics: Metrics{ @@ -255,7 +255,7 @@ func Test_Cache_set(t *testing.T) { }, }, "Set with existing key and no eviction": { - MaxCost: 500, + MaxCost: 50, Key: existingKey, TTL: DefaultTTL, Metrics: Metrics{ @@ -264,7 +264,7 @@ func Test_Cache_set(t *testing.T) { }, }, "Set with new key and eviction caused by exhausted cost": { - MaxCost: 400, + MaxCost: 40, Key: newKey, TTL: DefaultTTL, Metrics: Metrics{ @@ -1266,7 +1266,7 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c.options.costsCalcFunc = func(key string, item string) uint64 { // 72 bytes are used by the Item struct // 2 * 16 bytes are used by the used string headers (key and item) - return uint64(104 + len(key) + len(item)) + return uint64(len(item)) } } c.items.values = make(map[string]*list.Element) From 8965baf94864b9690bfcca79cbed9c2f3bc267d1 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:56:22 +0100 Subject: [PATCH 15/35] CostCalcFunc renamed to CostFunc --- options.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/options.go b/options.go index 0ee1354..7895224 100644 --- a/options.go +++ b/options.go @@ -15,15 +15,15 @@ func (fn optionFunc[K, V]) apply(opts *options[K, V]) { fn(opts) } -// CostCalcFunc is used to calculate the cost of the key and the item to be +// CostFunc is used to calculate the cost of the key and the item to be // inserted into the cache. -type CostCalcFunc[K comparable, V any] func(key K, item V) uint64 +type CostFunc[K comparable, V any] func(key K, item V) uint64 // options holds all available cache configuration options. type options[K comparable, V any] struct { capacity uint64 totalCost uint64 - costsCalcFunc CostCalcFunc[K, V] + costsCalcFunc CostFunc[K, V] ttl time.Duration loader Loader[K, V] disableTouchOnHit bool @@ -84,9 +84,9 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { // WithTotalCost sets the maximum cost the cache is allowed to use (e.g. the used memory). // The actual cost calculation for each inserted item happens by making use of the -// callback CostCalcFunc. +// callback CostFunc. // If used together with WithCapacity, WithTotalCost overrules the maximum capacity. -func WithTotalCost[K comparable, V any](s uint64, callback CostCalcFunc[K, V]) Option[K, V] { +func WithTotalCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.totalCost = s opts.costsCalcFunc = callback From 58dcb9094cf3d757d45c43637d78da098c018924 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:57:38 +0100 Subject: [PATCH 16/35] costsCalcFunc renamed to costFunc --- cache.go | 8 ++++---- cache_test.go | 4 ++-- options.go | 4 ++-- options_test.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index 1ad16ad..233f494 100644 --- a/cache.go +++ b/cache.go @@ -143,8 +143,8 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { item.update(value, ttl) if c.options.totalCost != 0 { - oldItemCosts := c.options.costsCalcFunc(key, oldValue) - newItemCosts := c.options.costsCalcFunc(key, value) + oldItemCosts := c.options.costFunc(key, oldValue) + newItemCosts := c.options.costFunc(key, value) c.cost = c.cost - oldItemCosts + newItemCosts @@ -174,7 +174,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.updateExpirations(true, elem) if c.options.totalCost != 0 { - c.cost += c.options.costsCalcFunc(key, value) + c.cost += c.options.costFunc(key, value) for c.cost > c.options.totalCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) @@ -283,7 +283,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { delete(c.items.values, item.key) if c.options.totalCost != 0 { - c.cost -= c.options.costsCalcFunc(item.key, item.value) + c.cost -= c.options.costFunc(item.key, item.value) } c.items.lru.Remove(elems[i]) diff --git a/cache_test.go b/cache_test.go index b1a2e4b..562f235 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1263,7 +1263,7 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c.options.ttl = ttl if maxCost != 0 { c.options.totalCost = maxCost - c.options.costsCalcFunc = func(key string, item string) uint64 { + c.options.costFunc = func(key string, item string) uint64 { // 72 bytes are used by the Item struct // 2 * 16 bytes are used by the used string headers (key and item) return uint64(len(item)) @@ -1295,7 +1295,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.expQueue.push(elem) if c.options.totalCost != 0 { - c.cost += c.options.costsCalcFunc(key, value) + c.cost += c.options.costFunc(key, value) } } } diff --git a/options.go b/options.go index 7895224..737fe82 100644 --- a/options.go +++ b/options.go @@ -23,7 +23,7 @@ type CostFunc[K comparable, V any] func(key K, item V) uint64 type options[K comparable, V any] struct { capacity uint64 totalCost uint64 - costsCalcFunc CostFunc[K, V] + costFunc CostFunc[K, V] ttl time.Duration loader Loader[K, V] disableTouchOnHit bool @@ -89,6 +89,6 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { func WithTotalCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.totalCost = s - opts.costsCalcFunc = callback + opts.costFunc = callback }) } diff --git a/options_test.go b/options_test.go index 016310a..8b9a177 100644 --- a/options_test.go +++ b/options_test.go @@ -75,5 +75,5 @@ func Test_WithTotalCost(t *testing.T) { WithTotalCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) assert.Equal(t, uint64(1024), opts.totalCost) - assert.Equal(t, uint64(1), opts.costsCalcFunc("test", "foo")) + assert.Equal(t, uint64(1), opts.costFunc("test", "foo")) } From 23cfbf01362a94e875feac545386aa9db5a14c26 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:58:58 +0100 Subject: [PATCH 17/35] updateExpirations moved --- cache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cache.go b/cache.go index 233f494..53eaa41 100644 --- a/cache.go +++ b/cache.go @@ -142,6 +142,8 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { oldValue := item.value item.update(value, ttl) + c.updateExpirations(false, elem) + if c.options.totalCost != 0 { oldItemCosts := c.options.costFunc(key, oldValue) newItemCosts := c.options.costFunc(key, value) @@ -153,8 +155,6 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { } } - c.updateExpirations(false, elem) - return item } From 08a47e8ee60b66c313b34ac5e84719f065f45f50 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 12:59:50 +0100 Subject: [PATCH 18/35] totalCost renamed to maxCost --- cache.go | 10 +++++----- cache_test.go | 4 ++-- options.go | 4 ++-- options_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cache.go b/cache.go index 53eaa41..183ee16 100644 --- a/cache.go +++ b/cache.go @@ -144,13 +144,13 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.updateExpirations(false, elem) - if c.options.totalCost != 0 { + if c.options.maxCost != 0 { oldItemCosts := c.options.costFunc(key, oldValue) newItemCosts := c.options.costFunc(key, value) c.cost = c.cost - oldItemCosts + newItemCosts - for c.cost > c.options.totalCost { + for c.cost > c.options.maxCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -173,10 +173,10 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.items.values[key] = elem c.updateExpirations(true, elem) - if c.options.totalCost != 0 { + if c.options.maxCost != 0 { c.cost += c.options.costFunc(key, value) - for c.cost > c.options.totalCost { + for c.cost > c.options.maxCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) } } @@ -282,7 +282,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { item := elems[i].Value.(*Item[K, V]) delete(c.items.values, item.key) - if c.options.totalCost != 0 { + if c.options.maxCost != 0 { c.cost -= c.options.costFunc(item.key, item.value) } diff --git a/cache_test.go b/cache_test.go index 562f235..941fa79 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1262,7 +1262,7 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c := &Cache[string, string]{} c.options.ttl = ttl if maxCost != 0 { - c.options.totalCost = maxCost + c.options.maxCost = maxCost c.options.costFunc = func(key string, item string) uint64 { // 72 bytes are used by the Item struct // 2 * 16 bytes are used by the used string headers (key and item) @@ -1294,7 +1294,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.values[key] = elem c.items.expQueue.push(elem) - if c.options.totalCost != 0 { + if c.options.maxCost != 0 { c.cost += c.options.costFunc(key, value) } } diff --git a/options.go b/options.go index 737fe82..26191ca 100644 --- a/options.go +++ b/options.go @@ -22,7 +22,7 @@ type CostFunc[K comparable, V any] func(key K, item V) uint64 // options holds all available cache configuration options. type options[K comparable, V any] struct { capacity uint64 - totalCost uint64 + maxCost uint64 costFunc CostFunc[K, V] ttl time.Duration loader Loader[K, V] @@ -88,7 +88,7 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { // If used together with WithCapacity, WithTotalCost overrules the maximum capacity. func WithTotalCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { - opts.totalCost = s + opts.maxCost = s opts.costFunc = callback }) } diff --git a/options_test.go b/options_test.go index 8b9a177..4e5113d 100644 --- a/options_test.go +++ b/options_test.go @@ -74,6 +74,6 @@ func Test_WithTotalCost(t *testing.T) { WithTotalCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) - assert.Equal(t, uint64(1024), opts.totalCost) + assert.Equal(t, uint64(1024), opts.maxCost) assert.Equal(t, uint64(1), opts.costFunc("test", "foo")) } From c594b8dd9d29e586a9da977175797c49802bc6b6 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 13:01:14 +0100 Subject: [PATCH 19/35] WithTotalCost option renamed to WithMaxCost --- options.go | 5 ++--- options_test.go | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/options.go b/options.go index 26191ca..199be65 100644 --- a/options.go +++ b/options.go @@ -82,11 +82,10 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { }) } -// WithTotalCost sets the maximum cost the cache is allowed to use (e.g. the used memory). +// WithMaxCost sets the maximum cost the cache is allowed to use (e.g. the used memory). // The actual cost calculation for each inserted item happens by making use of the // callback CostFunc. -// If used together with WithCapacity, WithTotalCost overrules the maximum capacity. -func WithTotalCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { +func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.maxCost = s opts.costFunc = callback diff --git a/options_test.go b/options_test.go index 4e5113d..9faf3a7 100644 --- a/options_test.go +++ b/options_test.go @@ -69,10 +69,10 @@ func Test_WithDisableTouchOnHit(t *testing.T) { assert.True(t, opts.disableTouchOnHit) } -func Test_WithTotalCost(t *testing.T) { +func Test_WithMaxCost(t *testing.T) { var opts options[string, string] - WithTotalCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) + WithMaxCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) assert.Equal(t, uint64(1024), opts.maxCost) assert.Equal(t, uint64(1), opts.costFunc("test", "foo")) From c4b0baa380ae94b84b698dcf0f180e10881b44db Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 13:02:35 +0100 Subject: [PATCH 20/35] example in README updated --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9249ca2..5c27bb3 100644 --- a/README.md +++ b/README.md @@ -143,13 +143,13 @@ func main() { ``` To restrict the cache's capacity based on criteria beyond the number -of items it can hold, the `ttlcache.WithTotalCost` option allows for +of items it can hold, the `ttlcache.WithMaxCost` option allows for implementing custom strategies. The following example demonstrates how to limit the maximum memory usage of a cache to 5MB: ```go func main() { cache := ttlcache.New[string, string]( - ttlcache.WithTotalCost[string, string](5120, func(key string, item string) uint64 { + ttlcache.WithMaxCost[string, string](5120, func(key string, item string) uint64 { // 72 (bytes) represent the memory occupied by the *ttlcache.Item structure // used to store the new value. // 16 (bytes) represent the memory footprint of a string header in Go, From 2a04fee843eb36dc235b99d850f7170538a66911 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 13:36:45 +0100 Subject: [PATCH 21/35] signature of the WithMaxCost option changed --- README.md | 13 +++++++------ cache.go | 14 ++++++++------ cache_test.go | 6 +++--- options.go | 2 +- options_test.go | 4 ++-- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5c27bb3..97580c4 100644 --- a/README.md +++ b/README.md @@ -147,14 +147,15 @@ of items it can hold, the `ttlcache.WithMaxCost` option allows for implementing custom strategies. The following example demonstrates how to limit the maximum memory usage of a cache to 5MB: ```go +import ( + "github.com/jellydator/ttlcache" + "github.com/DmitriyVTitov/size" +) + func main() { cache := ttlcache.New[string, string]( - ttlcache.WithMaxCost[string, string](5120, func(key string, item string) uint64 { - // 72 (bytes) represent the memory occupied by the *ttlcache.Item structure - // used to store the new value. - // 16 (bytes) represent the memory footprint of a string header in Go, - // as determined by unsafe.Sizeof. - return 72 + 16 + len(key) + 16 + len(item) + ttlcache.WithMaxCost[string, string](5120, func(item *ttlcache.Item[string, string]) uint64 { + return size.Of(item) }), ) diff --git a/cache.go b/cache.go index 183ee16..4b091be 100644 --- a/cache.go +++ b/cache.go @@ -137,17 +137,19 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { elem := c.get(key, false, true) if elem != nil { + var oldItemCosts uint64 // update/overwrite an existing item item := elem.Value.(*Item[K, V]) - oldValue := item.value + if c.options.maxCost != 0 { + oldItemCosts = c.options.costFunc(item) + } + item.update(value, ttl) c.updateExpirations(false, elem) if c.options.maxCost != 0 { - oldItemCosts := c.options.costFunc(key, oldValue) - newItemCosts := c.options.costFunc(key, value) - + newItemCosts := c.options.costFunc(item) c.cost = c.cost - oldItemCosts + newItemCosts for c.cost > c.options.maxCost { @@ -174,7 +176,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.updateExpirations(true, elem) if c.options.maxCost != 0 { - c.cost += c.options.costFunc(key, value) + c.cost += c.options.costFunc(item) for c.cost > c.options.maxCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) @@ -283,7 +285,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { delete(c.items.values, item.key) if c.options.maxCost != 0 { - c.cost -= c.options.costFunc(item.key, item.value) + c.cost -= c.options.costFunc(item) } c.items.lru.Remove(elems[i]) diff --git a/cache_test.go b/cache_test.go index 941fa79..cca02a9 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1263,10 +1263,10 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c.options.ttl = ttl if maxCost != 0 { c.options.maxCost = maxCost - c.options.costFunc = func(key string, item string) uint64 { + c.options.costFunc = func(item *Item[string, string]) uint64 { // 72 bytes are used by the Item struct // 2 * 16 bytes are used by the used string headers (key and item) - return uint64(len(item)) + return uint64(len(item.value)) } } c.items.values = make(map[string]*list.Element) @@ -1295,7 +1295,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.expQueue.push(elem) if c.options.maxCost != 0 { - c.cost += c.options.costFunc(key, value) + c.cost += c.options.costFunc(item) } } } diff --git a/options.go b/options.go index 199be65..a4bfd4d 100644 --- a/options.go +++ b/options.go @@ -17,7 +17,7 @@ func (fn optionFunc[K, V]) apply(opts *options[K, V]) { // CostFunc is used to calculate the cost of the key and the item to be // inserted into the cache. -type CostFunc[K comparable, V any] func(key K, item V) uint64 +type CostFunc[K comparable, V any] func(item *Item[K, V]) uint64 // options holds all available cache configuration options. type options[K comparable, V any] struct { diff --git a/options_test.go b/options_test.go index 9faf3a7..1f7e09a 100644 --- a/options_test.go +++ b/options_test.go @@ -72,8 +72,8 @@ func Test_WithDisableTouchOnHit(t *testing.T) { func Test_WithMaxCost(t *testing.T) { var opts options[string, string] - WithMaxCost[string, string](1024, func(key string, item string) uint64 { return 1 }).apply(&opts) + WithMaxCost[string, string](1024, func(item *Item[string, string]) uint64 { return 1 }).apply(&opts) assert.Equal(t, uint64(1024), opts.maxCost) - assert.Equal(t, uint64(1), opts.costFunc("test", "foo")) + assert.Equal(t, uint64(1), opts.costFunc(&Item[string, string]{key: "test", value: "foo"})) } From e36c05f0a35e9a57425982b802fd24984bbb872c Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 14 Nov 2024 13:52:51 +0100 Subject: [PATCH 22/35] readme updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97580c4..2cc7178 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ import ( func main() { cache := ttlcache.New[string, string]( ttlcache.WithMaxCost[string, string](5120, func(item *ttlcache.Item[string, string]) uint64 { - return size.Of(item) + return uint64(size.Of(item)) }), ) From ce9482fce5b4e1d39b93f9dbdfff818d6a238665 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:11:47 +0100 Subject: [PATCH 23/35] Item impl updated to hold the current cost and the corresponding cost calculation function; new constructor expecting options --- item.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/item.go b/item.go index 92eb73b..532da15 100644 --- a/item.go +++ b/item.go @@ -30,25 +30,34 @@ type Item[K comparable, V any] struct { // well, so locking this mutex would be redundant. // In other words, this mutex is only useful when these fields // are being read from the outside (e.g. in event functions). - mu sync.RWMutex - key K - value V - ttl time.Duration - expiresAt time.Time - queueIndex int - version int64 + mu sync.RWMutex + key K + value V + ttl time.Duration + expiresAt time.Time + queueIndex int + version int64 + calculateCost CostFunc[K, V] + cost uint64 } // NewItem creates a new cache item. +// Deprecated, use NewItemWithOpts instead func NewItem[K comparable, V any](key K, value V, ttl time.Duration, enableVersionTracking bool) *Item[K, V] { + return NewItemWithOpts(key, value, ttl, WithVersionTracking[K, V](enableVersionTracking)) +} + +// NewItemWithOpts creates a new cache item. +func NewItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opts ...ItemOption[K, V]) *Item[K, V] { item := &Item[K, V]{ - key: key, - value: value, - ttl: ttl, + key: key, + value: value, + ttl: ttl, + calculateCost: func(item *Item[K, V]) uint64 { return 0 }, } - if !enableVersionTracking { - item.version = -1 + for _, opt := range opts { + opt(item) } item.touch() @@ -62,6 +71,7 @@ func (item *Item[K, V]) update(value V, ttl time.Duration) { defer item.mu.Unlock() item.value = value + item.cost = item.calculateCost(item) // update version if enabled if item.version > -1 { From da1f07ee240d373101187e61c6e5ef9b6c8f7021 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:12:23 +0100 Subject: [PATCH 24/35] cache impl updated to make use of the new item properties --- cache.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index 4b091be..1a18d70 100644 --- a/cache.go +++ b/cache.go @@ -137,20 +137,16 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { elem := c.get(key, false, true) if elem != nil { - var oldItemCosts uint64 // update/overwrite an existing item item := elem.Value.(*Item[K, V]) - if c.options.maxCost != 0 { - oldItemCosts = c.options.costFunc(item) - } + oldItemCost := item.cost item.update(value, ttl) c.updateExpirations(false, elem) if c.options.maxCost != 0 { - newItemCosts := c.options.costFunc(item) - c.cost = c.cost - oldItemCosts + newItemCosts + c.cost = c.cost - oldItemCost + item.cost for c.cost > c.options.maxCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) @@ -170,13 +166,13 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { } // create a new item - item := NewItem(key, value, ttl, c.options.enableVersionTracking) + item := NewItemWithOpts(key, value, ttl, c.options.itemOpts...) elem = c.items.lru.PushFront(item) c.items.values[key] = elem c.updateExpirations(true, elem) if c.options.maxCost != 0 { - c.cost += c.options.costFunc(item) + c.cost += item.cost for c.cost > c.options.maxCost { c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) @@ -285,7 +281,7 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { delete(c.items.values, item.key) if c.options.maxCost != 0 { - c.cost -= c.options.costFunc(item) + c.cost -= item.cost } c.items.lru.Remove(elems[i]) From d275891aaf87bbb7c4cffa6f3e8be73ad43052f1 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:12:40 +0100 Subject: [PATCH 25/35] new item options --- options.go | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/options.go b/options.go index a4bfd4d..4f96936 100644 --- a/options.go +++ b/options.go @@ -21,13 +21,12 @@ type CostFunc[K comparable, V any] func(item *Item[K, V]) uint64 // options holds all available cache configuration options. type options[K comparable, V any] struct { - capacity uint64 - maxCost uint64 - costFunc CostFunc[K, V] - ttl time.Duration - loader Loader[K, V] - disableTouchOnHit bool - enableVersionTracking bool + capacity uint64 + maxCost uint64 + ttl time.Duration + loader Loader[K, V] + disableTouchOnHit bool + itemOpts []ItemOption[K, V] } // applyOptions applies the provided option values to the option struct. @@ -58,7 +57,7 @@ func WithTTL[K comparable, V any](ttl time.Duration) Option[K, V] { // It has no effect when used with Get(). func WithVersion[K comparable, V any](enable bool) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { - opts.enableVersionTracking = enable + opts.itemOpts = append(opts.itemOpts, WithVersionTracking[K, V](enable)) }) } @@ -88,6 +87,32 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.maxCost = s - opts.costFunc = callback + opts.itemOpts = append(opts.itemOpts, WithCostFunc[K, V](callback)) }) } + +// ItemOption represents an option to be applied to an Item on creation +type ItemOption[K comparable, V any] func(item *Item[K, V]) + +// WithVersionTracking deactivates ot activates item version tracking. +// If version tracking is disabled, the version is always -1. +// It has no effect when used with Get(). +func WithVersionTracking[K comparable, V any](enable bool) ItemOption[K, V] { + return func(item *Item[K, V]) { + if enable { + item.version = 0 + } else { + item.version = -1 + } + } +} + +// WithCostFunc configures the cost calculation function for an item +func WithCostFunc[K comparable, V any](costFunc CostFunc[K, V]) ItemOption[K, V] { + return func(item *Item[K, V]) { + if costFunc != nil { + item.calculateCost = costFunc + item.cost = costFunc(item) + } + } +} From d20ea4d3be2210dd205765472aa2e51b7beeaa8d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:13:02 +0100 Subject: [PATCH 26/35] tests for the new options --- options_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/options_test.go b/options_test.go index 1f7e09a..913a0f3 100644 --- a/options_test.go +++ b/options_test.go @@ -5,9 +5,12 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_optionFunc_apply(t *testing.T) { + t.Parallel() + var called bool optionFunc[string, string](func(_ *options[string, string]) { @@ -17,6 +20,8 @@ func Test_optionFunc_apply(t *testing.T) { } func Test_applyOptions(t *testing.T) { + t.Parallel() + var opts options[string, string] applyOptions(&opts, @@ -29,6 +34,8 @@ func Test_applyOptions(t *testing.T) { } func Test_WithCapacity(t *testing.T) { + t.Parallel() + var opts options[string, string] WithCapacity[string, string](12).apply(&opts) @@ -36,6 +43,8 @@ func Test_WithCapacity(t *testing.T) { } func Test_WithTTL(t *testing.T) { + t.Parallel() + var opts options[string, string] WithTTL[string, string](time.Hour).apply(&opts) @@ -43,16 +52,26 @@ func Test_WithTTL(t *testing.T) { } func Test_WithVersion(t *testing.T) { + t.Parallel() + var opts options[string, string] + var item Item[string, string] WithVersion[string, string](true).apply(&opts) - assert.Equal(t, true, opts.enableVersionTracking) + assert.Len(t, opts.itemOpts, 1) + opts.itemOpts[0](&item) + assert.Equal(t, int64(0), item.version) + opts.itemOpts = []ItemOption[string, string]{} WithVersion[string, string](false).apply(&opts) - assert.Equal(t, false, opts.enableVersionTracking) + assert.Len(t, opts.itemOpts, 1) + opts.itemOpts[0](&item) + assert.Equal(t, int64(-1), item.version) } func Test_WithLoader(t *testing.T) { + t.Parallel() + var opts options[string, string] l := LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { @@ -63,6 +82,8 @@ func Test_WithLoader(t *testing.T) { } func Test_WithDisableTouchOnHit(t *testing.T) { + t.Parallel() + var opts options[string, string] WithDisableTouchOnHit[string, string]().apply(&opts) @@ -70,10 +91,44 @@ func Test_WithDisableTouchOnHit(t *testing.T) { } func Test_WithMaxCost(t *testing.T) { + t.Parallel() + var opts options[string, string] + var item Item[string, string] WithMaxCost[string, string](1024, func(item *Item[string, string]) uint64 { return 1 }).apply(&opts) assert.Equal(t, uint64(1024), opts.maxCost) - assert.Equal(t, uint64(1), opts.costFunc(&Item[string, string]{key: "test", value: "foo"})) + assert.Len(t, opts.itemOpts, 1) + opts.itemOpts[0](&item) + assert.Equal(t, uint64(1), item.cost) + assert.NotNil(t, item.calculateCost) +} + +func Test_WithVersionTracking(t *testing.T) { + t.Parallel() + + var item Item[string, string] + + opt := WithVersionTracking[string, string](false) + opt(&item) + assert.Equal(t, int64(-1), item.version) + + opt = WithVersionTracking[string, string](true) + opt(&item) + assert.Equal(t, int64(0), item.version) +} + +func Test_WithCostFunc(t *testing.T) { + t.Parallel() + + var item Item[string, string] + + opt := WithCostFunc[string, string](func(item *Item[string, string]) uint64 { + return 10 + }) + opt(&item) + assert.Equal(t, uint64(10), item.cost) + require.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(10), item.calculateCost(&item)) } From 692baf02ce32488c2a761ce74497fdd59fae9875 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:13:32 +0100 Subject: [PATCH 27/35] new Item related tests --- item_test.go | 187 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 20 deletions(-) diff --git a/item_test.go b/item_test.go index 7e7699e..6f6d91b 100644 --- a/item_test.go +++ b/item_test.go @@ -9,6 +9,8 @@ import ( ) func Test_NewItem(t *testing.T) { + t.Parallel() + item := NewItem("key", 123, time.Hour, false) require.NotNil(t, item) assert.Equal(t, "key", item.key) @@ -18,33 +20,166 @@ func Test_NewItem(t *testing.T) { assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) } -func Test_Item_update(t *testing.T) { - item := Item[string, string]{ - expiresAt: time.Now().Add(-time.Hour), - value: "hello", - version: 0, +func Test_NewItemWithOpts(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opts []ItemOption[string, int] + assert func(t *testing.T, item *Item[string, int]) + }{ + { + uc: "item without any options", + assert: func(t *testing.T, item *Item[string, int]) { + assert.Equal(t, int64(0), item.version) + assert.Equal(t, uint64(0), item.cost) + require.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(0), item.calculateCost(item)) + }, + }, + { + uc: "item with version tracking disabled", + opts: []ItemOption[string, int]{ + WithVersionTracking[string, int](false), + }, + assert: func(t *testing.T, item *Item[string, int]) { + assert.Equal(t, int64(-1), item.version) + assert.Equal(t, uint64(0), item.cost) + require.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(0), item.calculateCost(item)) + }, + }, + { + uc: "item with version tracking explicitly enabled", + opts: []ItemOption[string, int]{ + WithVersionTracking[string, int](true), + }, + assert: func(t *testing.T, item *Item[string, int]) { + assert.Equal(t, int64(0), item.version) + assert.Equal(t, uint64(0), item.cost) + require.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(0), item.calculateCost(item)) + }, + }, + { + uc: "item with cost calculation", + opts: []ItemOption[string, int]{ + WithCostFunc[string, int](func(item *Item[string, int]) uint64 { return 5 }), + }, + assert: func(t *testing.T, item *Item[string, int]) { + assert.Equal(t, int64(0), item.version) + assert.Equal(t, uint64(5), item.cost) + require.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(5), item.calculateCost(item)) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + item := NewItemWithOpts("key", 123, time.Hour, tc.opts...) + require.NotNil(t, item) + assert.Equal(t, "key", item.key) + assert.Equal(t, 123, item.value) + assert.Equal(t, time.Hour, item.ttl) + assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) + tc.assert(t, item) + }) } +} - item.update("test", time.Hour) - assert.Equal(t, "test", item.value) - assert.Equal(t, time.Hour, item.ttl) - assert.Equal(t, int64(1), item.version) - assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) +func Test_Item_update(t *testing.T) { + t.Parallel() - item.update("previous ttl", PreviousOrDefaultTTL) - assert.Equal(t, "previous ttl", item.value) - assert.Equal(t, time.Hour, item.ttl) - assert.Equal(t, int64(2), item.version) - assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) + initialTTL := -1 * time.Hour + newValue := "world" + + for _, tc := range []struct { + uc string + opts []ItemOption[string, string] + ttl time.Duration + assert func(t *testing.T, item *Item[string, string]) + }{ + { + uc: "with expiration in an hour", + ttl: time.Hour, + assert: func(t *testing.T, item *Item[string, string]) { + t.Helper() + + assert.Equal(t, uint64(0), item.cost) + assert.Equal(t, time.Hour, item.ttl) + assert.Equal(t, int64(1), item.version) + assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) + }, + }, + { + uc: "with previous or default ttl", + ttl: PreviousOrDefaultTTL, + assert: func(t *testing.T, item *Item[string, string]) { + t.Helper() + + assert.Equal(t, uint64(0), item.cost) + assert.Equal(t, initialTTL, item.ttl) + assert.Equal(t, int64(1), item.version) + }, + }, + { + uc: "with no ttl", + ttl: NoTTL, + assert: func(t *testing.T, item *Item[string, string]) { + t.Helper() + + assert.Equal(t, uint64(0), item.cost) + assert.Equal(t, NoTTL, item.ttl) + assert.Equal(t, int64(1), item.version) + assert.Zero(t, item.expiresAt) + }, + }, + { + uc: "without version tracking", + opts: []ItemOption[string, string]{ + WithVersionTracking[string, string](false), + }, + ttl: time.Hour, + assert: func(t *testing.T, item *Item[string, string]) { + t.Helper() + + assert.Equal(t, uint64(0), item.cost) + assert.Equal(t, time.Hour, item.ttl) + assert.Equal(t, int64(-1), item.version) + assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) + }, + }, + { + uc: "with version calculation and version tracking", + opts: []ItemOption[string, string]{ + WithVersionTracking[string, string](true), + WithCostFunc[string, string](func(item *Item[string, string]) uint64 { return uint64(len(item.value)) }), + }, + ttl: time.Hour, + assert: func(t *testing.T, item *Item[string, string]) { + t.Helper() + + assert.Equal(t, uint64(len(newValue)), item.cost) + assert.Equal(t, time.Hour, item.ttl) + assert.Equal(t, int64(1), item.version) + assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + item := NewItemWithOpts[string, string]("test", "hello", initialTTL, tc.opts...) + + item.update(newValue, tc.ttl) + + assert.Equal(t, newValue, item.value) + tc.assert(t, item) + }) + } - item.update("hi", NoTTL) - assert.Equal(t, "hi", item.value) - assert.Equal(t, NoTTL, item.ttl) - assert.Equal(t, int64(3), item.version) - assert.Zero(t, item.expiresAt) } func Test_Item_touch(t *testing.T) { + t.Parallel() + var item Item[string, string] item.touch() assert.Equal(t, int64(0), item.version) @@ -57,6 +192,8 @@ func Test_Item_touch(t *testing.T) { } func Test_Item_IsExpired(t *testing.T) { + t.Parallel() + // no ttl item := Item[string, string]{ expiresAt: time.Now().Add(-time.Hour), @@ -74,6 +211,8 @@ func Test_Item_IsExpired(t *testing.T) { } func Test_Item_Key(t *testing.T) { + t.Parallel() + item := Item[string, string]{ key: "test", } @@ -82,6 +221,8 @@ func Test_Item_Key(t *testing.T) { } func Test_Item_Value(t *testing.T) { + t.Parallel() + item := Item[string, string]{ value: "test", } @@ -90,6 +231,8 @@ func Test_Item_Value(t *testing.T) { } func Test_Item_TTL(t *testing.T) { + t.Parallel() + item := Item[string, string]{ ttl: time.Hour, } @@ -98,6 +241,8 @@ func Test_Item_TTL(t *testing.T) { } func Test_Item_ExpiresAt(t *testing.T) { + t.Parallel() + now := time.Now() item := Item[string, string]{ expiresAt: now, @@ -107,6 +252,8 @@ func Test_Item_ExpiresAt(t *testing.T) { } func Test_Item_Version(t *testing.T) { + t.Parallel() + item := Item[string, string]{version: 5} assert.Equal(t, int64(5), item.Version()) } From 2308b728608f26c9f3313f784abf6d5d17847eb9 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 17 Nov 2024 20:14:07 +0100 Subject: [PATCH 28/35] cache tests fixed to make them compile --- cache_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cache_test.go b/cache_test.go index cca02a9..fc2d027 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1261,14 +1261,17 @@ func Test_SuppressedLoader_Load(t *testing.T) { func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, string] { c := &Cache[string, string]{} c.options.ttl = ttl + c.options.itemOpts = append(c.options.itemOpts, + WithVersionTracking[string, string](false)) + if maxCost != 0 { c.options.maxCost = maxCost - c.options.costFunc = func(item *Item[string, string]) uint64 { - // 72 bytes are used by the Item struct - // 2 * 16 bytes are used by the used string headers (key and item) - return uint64(len(item.value)) - } + c.options.itemOpts = append(c.options.itemOpts, + WithCostFunc[string, string](func(item *Item[string, string]) uint64 { + return uint64(len(item.value)) + })) } + c.items.values = make(map[string]*list.Element) c.items.lru = list.New() c.items.expQueue = newExpirationQueue[string, string]() @@ -1284,18 +1287,15 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { for i, key := range keys { value := fmt.Sprint("value of", key) - item := NewItem( + item := NewItemWithOpts( key, value, ttl+time.Duration(i)*time.Minute, - false, + c.options.itemOpts..., ) elem := c.items.lru.PushFront(item) c.items.values[key] = elem c.items.expQueue.push(elem) - if c.options.maxCost != 0 { - c.cost += c.options.costFunc(item) - } } } From fd2c7ad2647eb20359e48df113a4511b0c5826c8 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 27 Nov 2024 01:40:02 +0100 Subject: [PATCH 29/35] item option implementation updated to become a private interface --- item.go | 2 +- options.go | 20 +++++++++++++++----- options_test.go | 12 ++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/item.go b/item.go index 532da15..86d1c99 100644 --- a/item.go +++ b/item.go @@ -57,7 +57,7 @@ func NewItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opt } for _, opt := range opts { - opt(item) + opt.apply(item) } item.touch() diff --git a/options.go b/options.go index 4f96936..b4381be 100644 --- a/options.go +++ b/options.go @@ -92,27 +92,37 @@ func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[ } // ItemOption represents an option to be applied to an Item on creation -type ItemOption[K comparable, V any] func(item *Item[K, V]) +type ItemOption[K comparable, V any] interface { + apply(item *Item[K, V]) +} + +// itemOptionFunc wraps a function and implements the ItemOption interface. +type itemOptionFunc[K comparable, V any] func(*Item[K, V]) + +// apply calls the wrapped function. +func (fn itemOptionFunc[K, V]) apply(item *Item[K, V]) { + fn(item) +} // WithVersionTracking deactivates ot activates item version tracking. // If version tracking is disabled, the version is always -1. // It has no effect when used with Get(). func WithVersionTracking[K comparable, V any](enable bool) ItemOption[K, V] { - return func(item *Item[K, V]) { + return itemOptionFunc[K, V](func(item *Item[K, V]) { if enable { item.version = 0 } else { item.version = -1 } - } + }) } // WithCostFunc configures the cost calculation function for an item func WithCostFunc[K comparable, V any](costFunc CostFunc[K, V]) ItemOption[K, V] { - return func(item *Item[K, V]) { + return itemOptionFunc[K, V](func(item *Item[K, V]) { if costFunc != nil { item.calculateCost = costFunc item.cost = costFunc(item) } - } + }) } diff --git a/options_test.go b/options_test.go index 913a0f3..0f23f99 100644 --- a/options_test.go +++ b/options_test.go @@ -59,13 +59,13 @@ func Test_WithVersion(t *testing.T) { WithVersion[string, string](true).apply(&opts) assert.Len(t, opts.itemOpts, 1) - opts.itemOpts[0](&item) + opts.itemOpts[0].apply(&item) assert.Equal(t, int64(0), item.version) opts.itemOpts = []ItemOption[string, string]{} WithVersion[string, string](false).apply(&opts) assert.Len(t, opts.itemOpts, 1) - opts.itemOpts[0](&item) + opts.itemOpts[0].apply(&item) assert.Equal(t, int64(-1), item.version) } @@ -100,7 +100,7 @@ func Test_WithMaxCost(t *testing.T) { assert.Equal(t, uint64(1024), opts.maxCost) assert.Len(t, opts.itemOpts, 1) - opts.itemOpts[0](&item) + opts.itemOpts[0].apply(&item) assert.Equal(t, uint64(1), item.cost) assert.NotNil(t, item.calculateCost) } @@ -111,11 +111,11 @@ func Test_WithVersionTracking(t *testing.T) { var item Item[string, string] opt := WithVersionTracking[string, string](false) - opt(&item) + opt.apply(&item) assert.Equal(t, int64(-1), item.version) opt = WithVersionTracking[string, string](true) - opt(&item) + opt.apply(&item) assert.Equal(t, int64(0), item.version) } @@ -127,7 +127,7 @@ func Test_WithCostFunc(t *testing.T) { opt := WithCostFunc[string, string](func(item *Item[string, string]) uint64 { return 10 }) - opt(&item) + opt.apply(&item) assert.Equal(t, uint64(10), item.cost) require.NotNil(t, item.calculateCost) assert.Equal(t, uint64(10), item.calculateCost(&item)) From 2a15bc1ac16774a362ad1603e76853b6842f8a94 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 27 Nov 2024 01:40:21 +0100 Subject: [PATCH 30/35] cache set test updated --- cache_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cache_test.go b/cache_test.go index fc2d027..97b93f5 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1297,5 +1297,8 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { c.items.values[key] = elem c.items.expQueue.push(elem) + if c.options.maxCost != 0 { + c.cost += item.cost + } } } From eebe1a86117cfd3ef76161170faaceedc27ed4e3 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 29 Nov 2024 18:25:02 +0100 Subject: [PATCH 31/35] made new item related functions private + eviction reason renamed --- cache.go | 8 ++++---- cache_test.go | 8 ++++---- item.go | 7 +++---- item_test.go | 30 +++++++++++++++--------------- options.go | 20 ++++++++++---------- options_test.go | 8 ++++---- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/cache.go b/cache.go index 1a18d70..24ca09a 100644 --- a/cache.go +++ b/cache.go @@ -15,7 +15,7 @@ const ( EvictionReasonDeleted EvictionReason = iota + 1 EvictionReasonCapacityReached EvictionReasonExpired - EvictionReasonTotalCostExceeded + EvictionReasonMaxCostExceeded ) // EvictionReason is used to specify why a certain item was @@ -149,7 +149,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.cost = c.cost - oldItemCost + item.cost for c.cost > c.options.maxCost { - c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) + c.evict(EvictionReasonMaxCostExceeded, c.items.lru.Back()) } } @@ -166,7 +166,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { } // create a new item - item := NewItemWithOpts(key, value, ttl, c.options.itemOpts...) + item := newItemWithOpts(key, value, ttl, c.options.itemOpts...) elem = c.items.lru.PushFront(item) c.items.values[key] = elem c.updateExpirations(true, elem) @@ -175,7 +175,7 @@ func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { c.cost += item.cost for c.cost > c.options.maxCost { - c.evict(EvictionReasonTotalCostExceeded, c.items.lru.Back()) + c.evict(EvictionReasonMaxCostExceeded, c.items.lru.Back()) } } diff --git a/cache_test.go b/cache_test.go index 97b93f5..ba58c3f 100644 --- a/cache_test.go +++ b/cache_test.go @@ -298,7 +298,7 @@ func Test_Cache_set(t *testing.T) { cache.events.insertion.fns[2] = cache.events.insertion.fns[1] cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { if c.MaxCost != 0 { - assert.Equal(t, EvictionReasonTotalCostExceeded, r) + assert.Equal(t, EvictionReasonMaxCostExceeded, r) } else { assert.Equal(t, EvictionReasonCapacityReached, r) } @@ -1262,12 +1262,12 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, c := &Cache[string, string]{} c.options.ttl = ttl c.options.itemOpts = append(c.options.itemOpts, - WithVersionTracking[string, string](false)) + withVersionTracking[string, string](false)) if maxCost != 0 { c.options.maxCost = maxCost c.options.itemOpts = append(c.options.itemOpts, - WithCostFunc[string, string](func(item *Item[string, string]) uint64 { + withCostFunc[string, string](func(item *Item[string, string]) uint64 { return uint64(len(item.value)) })) } @@ -1287,7 +1287,7 @@ func prepCache(maxCost uint64, ttl time.Duration, keys ...string) *Cache[string, func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { for i, key := range keys { value := fmt.Sprint("value of", key) - item := NewItemWithOpts( + item := newItemWithOpts( key, value, ttl+time.Duration(i)*time.Minute, diff --git a/item.go b/item.go index 86d1c99..3268076 100644 --- a/item.go +++ b/item.go @@ -42,13 +42,12 @@ type Item[K comparable, V any] struct { } // NewItem creates a new cache item. -// Deprecated, use NewItemWithOpts instead func NewItem[K comparable, V any](key K, value V, ttl time.Duration, enableVersionTracking bool) *Item[K, V] { - return NewItemWithOpts(key, value, ttl, WithVersionTracking[K, V](enableVersionTracking)) + return newItemWithOpts(key, value, ttl, withVersionTracking[K, V](enableVersionTracking)) } -// NewItemWithOpts creates a new cache item. -func NewItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opts ...ItemOption[K, V]) *Item[K, V] { +// newItemWithOpts creates a new cache item. +func newItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opts ...itemOption[K, V]) *Item[K, V] { item := &Item[K, V]{ key: key, value: value, diff --git a/item_test.go b/item_test.go index 6f6d91b..7c41924 100644 --- a/item_test.go +++ b/item_test.go @@ -25,7 +25,7 @@ func Test_NewItemWithOpts(t *testing.T) { for _, tc := range []struct { uc string - opts []ItemOption[string, int] + opts []itemOption[string, int] assert func(t *testing.T, item *Item[string, int]) }{ { @@ -39,8 +39,8 @@ func Test_NewItemWithOpts(t *testing.T) { }, { uc: "item with version tracking disabled", - opts: []ItemOption[string, int]{ - WithVersionTracking[string, int](false), + opts: []itemOption[string, int]{ + withVersionTracking[string, int](false), }, assert: func(t *testing.T, item *Item[string, int]) { assert.Equal(t, int64(-1), item.version) @@ -51,8 +51,8 @@ func Test_NewItemWithOpts(t *testing.T) { }, { uc: "item with version tracking explicitly enabled", - opts: []ItemOption[string, int]{ - WithVersionTracking[string, int](true), + opts: []itemOption[string, int]{ + withVersionTracking[string, int](true), }, assert: func(t *testing.T, item *Item[string, int]) { assert.Equal(t, int64(0), item.version) @@ -63,8 +63,8 @@ func Test_NewItemWithOpts(t *testing.T) { }, { uc: "item with cost calculation", - opts: []ItemOption[string, int]{ - WithCostFunc[string, int](func(item *Item[string, int]) uint64 { return 5 }), + opts: []itemOption[string, int]{ + withCostFunc[string, int](func(item *Item[string, int]) uint64 { return 5 }), }, assert: func(t *testing.T, item *Item[string, int]) { assert.Equal(t, int64(0), item.version) @@ -75,7 +75,7 @@ func Test_NewItemWithOpts(t *testing.T) { }, } { t.Run(tc.uc, func(t *testing.T) { - item := NewItemWithOpts("key", 123, time.Hour, tc.opts...) + item := newItemWithOpts("key", 123, time.Hour, tc.opts...) require.NotNil(t, item) assert.Equal(t, "key", item.key) assert.Equal(t, 123, item.value) @@ -94,7 +94,7 @@ func Test_Item_update(t *testing.T) { for _, tc := range []struct { uc string - opts []ItemOption[string, string] + opts []itemOption[string, string] ttl time.Duration assert func(t *testing.T, item *Item[string, string]) }{ @@ -135,8 +135,8 @@ func Test_Item_update(t *testing.T) { }, { uc: "without version tracking", - opts: []ItemOption[string, string]{ - WithVersionTracking[string, string](false), + opts: []itemOption[string, string]{ + withVersionTracking[string, string](false), }, ttl: time.Hour, assert: func(t *testing.T, item *Item[string, string]) { @@ -150,9 +150,9 @@ func Test_Item_update(t *testing.T) { }, { uc: "with version calculation and version tracking", - opts: []ItemOption[string, string]{ - WithVersionTracking[string, string](true), - WithCostFunc[string, string](func(item *Item[string, string]) uint64 { return uint64(len(item.value)) }), + opts: []itemOption[string, string]{ + withVersionTracking[string, string](true), + withCostFunc[string, string](func(item *Item[string, string]) uint64 { return uint64(len(item.value)) }), }, ttl: time.Hour, assert: func(t *testing.T, item *Item[string, string]) { @@ -166,7 +166,7 @@ func Test_Item_update(t *testing.T) { }, } { t.Run(tc.uc, func(t *testing.T) { - item := NewItemWithOpts[string, string]("test", "hello", initialTTL, tc.opts...) + item := newItemWithOpts[string, string]("test", "hello", initialTTL, tc.opts...) item.update(newValue, tc.ttl) diff --git a/options.go b/options.go index b4381be..287821b 100644 --- a/options.go +++ b/options.go @@ -26,7 +26,7 @@ type options[K comparable, V any] struct { ttl time.Duration loader Loader[K, V] disableTouchOnHit bool - itemOpts []ItemOption[K, V] + itemOpts []itemOption[K, V] } // applyOptions applies the provided option values to the option struct. @@ -57,7 +57,7 @@ func WithTTL[K comparable, V any](ttl time.Duration) Option[K, V] { // It has no effect when used with Get(). func WithVersion[K comparable, V any](enable bool) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { - opts.itemOpts = append(opts.itemOpts, WithVersionTracking[K, V](enable)) + opts.itemOpts = append(opts.itemOpts, withVersionTracking[K, V](enable)) }) } @@ -87,16 +87,16 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { return optionFunc[K, V](func(opts *options[K, V]) { opts.maxCost = s - opts.itemOpts = append(opts.itemOpts, WithCostFunc[K, V](callback)) + opts.itemOpts = append(opts.itemOpts, withCostFunc[K, V](callback)) }) } -// ItemOption represents an option to be applied to an Item on creation -type ItemOption[K comparable, V any] interface { +// itemOption represents an option to be applied to an Item on creation +type itemOption[K comparable, V any] interface { apply(item *Item[K, V]) } -// itemOptionFunc wraps a function and implements the ItemOption interface. +// itemOptionFunc wraps a function and implements the itemOption interface. type itemOptionFunc[K comparable, V any] func(*Item[K, V]) // apply calls the wrapped function. @@ -104,10 +104,10 @@ func (fn itemOptionFunc[K, V]) apply(item *Item[K, V]) { fn(item) } -// WithVersionTracking deactivates ot activates item version tracking. +// withVersionTracking deactivates ot activates item version tracking. // If version tracking is disabled, the version is always -1. // It has no effect when used with Get(). -func WithVersionTracking[K comparable, V any](enable bool) ItemOption[K, V] { +func withVersionTracking[K comparable, V any](enable bool) itemOption[K, V] { return itemOptionFunc[K, V](func(item *Item[K, V]) { if enable { item.version = 0 @@ -117,8 +117,8 @@ func WithVersionTracking[K comparable, V any](enable bool) ItemOption[K, V] { }) } -// WithCostFunc configures the cost calculation function for an item -func WithCostFunc[K comparable, V any](costFunc CostFunc[K, V]) ItemOption[K, V] { +// withCostFunc configures the cost calculation function for an item +func withCostFunc[K comparable, V any](costFunc CostFunc[K, V]) itemOption[K, V] { return itemOptionFunc[K, V](func(item *Item[K, V]) { if costFunc != nil { item.calculateCost = costFunc diff --git a/options_test.go b/options_test.go index 0f23f99..3d2db42 100644 --- a/options_test.go +++ b/options_test.go @@ -62,7 +62,7 @@ func Test_WithVersion(t *testing.T) { opts.itemOpts[0].apply(&item) assert.Equal(t, int64(0), item.version) - opts.itemOpts = []ItemOption[string, string]{} + opts.itemOpts = []itemOption[string, string]{} WithVersion[string, string](false).apply(&opts) assert.Len(t, opts.itemOpts, 1) opts.itemOpts[0].apply(&item) @@ -110,11 +110,11 @@ func Test_WithVersionTracking(t *testing.T) { var item Item[string, string] - opt := WithVersionTracking[string, string](false) + opt := withVersionTracking[string, string](false) opt.apply(&item) assert.Equal(t, int64(-1), item.version) - opt = WithVersionTracking[string, string](true) + opt = withVersionTracking[string, string](true) opt.apply(&item) assert.Equal(t, int64(0), item.version) } @@ -124,7 +124,7 @@ func Test_WithCostFunc(t *testing.T) { var item Item[string, string] - opt := WithCostFunc[string, string](func(item *Item[string, string]) uint64 { + opt := withCostFunc[string, string](func(item *Item[string, string]) uint64 { return 10 }) opt.apply(&item) From 5cb43bf6bac41c894f108681075e00dcc1f22484 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 29 Nov 2024 18:58:24 +0100 Subject: [PATCH 32/35] moved initial item cost calsulation to the constructor function --- item.go | 1 + options.go | 1 - options_test.go | 5 +++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/item.go b/item.go index 3268076..4dbc5df 100644 --- a/item.go +++ b/item.go @@ -60,6 +60,7 @@ func newItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opt } item.touch() + item.cost = item.calculateCost(item) return item } diff --git a/options.go b/options.go index 287821b..6607a09 100644 --- a/options.go +++ b/options.go @@ -122,7 +122,6 @@ func withCostFunc[K comparable, V any](costFunc CostFunc[K, V]) itemOption[K, V] return itemOptionFunc[K, V](func(item *Item[K, V]) { if costFunc != nil { item.calculateCost = costFunc - item.cost = costFunc(item) } }) } diff --git a/options_test.go b/options_test.go index 3d2db42..dfd2da5 100644 --- a/options_test.go +++ b/options_test.go @@ -101,8 +101,9 @@ func Test_WithMaxCost(t *testing.T) { assert.Equal(t, uint64(1024), opts.maxCost) assert.Len(t, opts.itemOpts, 1) opts.itemOpts[0].apply(&item) - assert.Equal(t, uint64(1), item.cost) + assert.Equal(t, uint64(0), item.cost) assert.NotNil(t, item.calculateCost) + assert.Equal(t, uint64(1), item.calculateCost(&item)) } func Test_WithVersionTracking(t *testing.T) { @@ -128,7 +129,7 @@ func Test_WithCostFunc(t *testing.T) { return 10 }) opt.apply(&item) - assert.Equal(t, uint64(10), item.cost) + assert.Equal(t, uint64(0), item.cost) require.NotNil(t, item.calculateCost) assert.Equal(t, uint64(10), item.calculateCost(&item)) } From e70f90d11d67956b9f9924b2a58e52fc4651a85d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 29 Nov 2024 19:01:48 +0100 Subject: [PATCH 33/35] test functions renamed to reflect the private nature of functions under test --- item_test.go | 2 +- options_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/item_test.go b/item_test.go index 7c41924..549b76d 100644 --- a/item_test.go +++ b/item_test.go @@ -20,7 +20,7 @@ func Test_NewItem(t *testing.T) { assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) } -func Test_NewItemWithOpts(t *testing.T) { +func Test_newItemWithOpts(t *testing.T) { t.Parallel() for _, tc := range []struct { diff --git a/options_test.go b/options_test.go index dfd2da5..478dc40 100644 --- a/options_test.go +++ b/options_test.go @@ -106,7 +106,7 @@ func Test_WithMaxCost(t *testing.T) { assert.Equal(t, uint64(1), item.calculateCost(&item)) } -func Test_WithVersionTracking(t *testing.T) { +func Test_withVersionTracking(t *testing.T) { t.Parallel() var item Item[string, string] @@ -120,7 +120,7 @@ func Test_WithVersionTracking(t *testing.T) { assert.Equal(t, int64(0), item.version) } -func Test_WithCostFunc(t *testing.T) { +func Test_withCostFunc(t *testing.T) { t.Parallel() var item Item[string, string] From 43fc03305263ac73e592bf363f3f9c0830774659 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 29 Nov 2024 19:32:08 +0100 Subject: [PATCH 34/35] version tracking fixed --- item.go | 1 + item_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/item.go b/item.go index 4dbc5df..a72add5 100644 --- a/item.go +++ b/item.go @@ -52,6 +52,7 @@ func newItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opt key: key, value: value, ttl: ttl, + version: -1, calculateCost: func(item *Item[K, V]) uint64 { return 0 }, } diff --git a/item_test.go b/item_test.go index 549b76d..02becb3 100644 --- a/item_test.go +++ b/item_test.go @@ -31,7 +31,7 @@ func Test_newItemWithOpts(t *testing.T) { { uc: "item without any options", assert: func(t *testing.T, item *Item[string, int]) { - assert.Equal(t, int64(0), item.version) + assert.Equal(t, int64(-1), item.version) assert.Equal(t, uint64(0), item.cost) require.NotNil(t, item.calculateCost) assert.Equal(t, uint64(0), item.calculateCost(item)) @@ -67,7 +67,7 @@ func Test_newItemWithOpts(t *testing.T) { withCostFunc[string, int](func(item *Item[string, int]) uint64 { return 5 }), }, assert: func(t *testing.T, item *Item[string, int]) { - assert.Equal(t, int64(0), item.version) + assert.Equal(t, int64(-1), item.version) assert.Equal(t, uint64(5), item.cost) require.NotNil(t, item.calculateCost) assert.Equal(t, uint64(5), item.calculateCost(item)) @@ -106,7 +106,7 @@ func Test_Item_update(t *testing.T) { assert.Equal(t, uint64(0), item.cost) assert.Equal(t, time.Hour, item.ttl) - assert.Equal(t, int64(1), item.version) + assert.Equal(t, int64(-1), item.version) assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute) }, }, @@ -118,7 +118,7 @@ func Test_Item_update(t *testing.T) { assert.Equal(t, uint64(0), item.cost) assert.Equal(t, initialTTL, item.ttl) - assert.Equal(t, int64(1), item.version) + assert.Equal(t, int64(-1), item.version) }, }, { @@ -129,12 +129,12 @@ func Test_Item_update(t *testing.T) { assert.Equal(t, uint64(0), item.cost) assert.Equal(t, NoTTL, item.ttl) - assert.Equal(t, int64(1), item.version) + assert.Equal(t, int64(-1), item.version) assert.Zero(t, item.expiresAt) }, }, { - uc: "without version tracking", + uc: "with version tracking explicitly disabled", opts: []itemOption[string, string]{ withVersionTracking[string, string](false), }, From 41f3351f74be610d40f62ca48fb4dbc082e86ad8 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 29 Nov 2024 19:40:17 +0100 Subject: [PATCH 35/35] small readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cc7178..a080035 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ func main() { To restrict the cache's capacity based on criteria beyond the number of items it can hold, the `ttlcache.WithMaxCost` option allows for implementing custom strategies. The following example demonstrates -how to limit the maximum memory usage of a cache to 5MB: +how to limit the maximum memory usage of a cache to 5KiB: ```go import ( "github.com/jellydator/ttlcache"