diff --git a/cache.go b/cache.go index 24ca09a..d9ccec3 100644 --- a/cache.go +++ b/cache.go @@ -71,7 +71,7 @@ func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] { c.events.insertion.fns = make(map[uint64]func(*Item[K, V])) c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[K, V])) - applyOptions(&c.options, opts...) + c.options = applyOptions(c.options, opts...) return c } @@ -233,7 +233,7 @@ func (c *Cache[K, V]) getWithOpts(key K, lockAndLoad bool, opts ...Option[K, V]) disableTouchOnHit: c.options.disableTouchOnHit, } - applyOptions(&getOpts, opts...) + getOpts = applyOptions(getOpts, opts...) if lockAndLoad { c.items.mu.Lock() @@ -388,7 +388,7 @@ func (c *Cache[K, V]) GetOrSet(key K, value V, opts ...Option[K, V]) (*Item[K, V setOpts := options[K, V]{ ttl: c.options.ttl, } - applyOptions(&setOpts, opts...) // used only to update the TTL + setOpts = applyOptions(setOpts, opts...) // used only to update the TTL item := c.set(key, value, setOpts.ttl) @@ -412,7 +412,7 @@ func (c *Cache[K, V]) GetAndDelete(key K, opts ...Option[K, V]) (*Item[K, V], bo getOpts := options[K, V]{ loader: c.options.loader, } - applyOptions(&getOpts, opts...) // used only to update the loader + getOpts = applyOptions(getOpts, opts...) // used only to update the loader if getOpts.loader != nil { item := getOpts.loader.Load(c, key) diff --git a/cache_test.go b/cache_test.go index ba58c3f..a9e7524 100644 --- a/cache_test.go +++ b/cache_test.go @@ -529,11 +529,12 @@ func Test_Cache_Set(t *testing.T) { func Test_Cache_Get(t *testing.T) { const notFoundKey, foundKey = "notfound", "test1" cc := map[string]struct { - Key string - DefaultOptions options[string, string] - CallOptions []Option[string, string] - Metrics Metrics - Result *Item[string, string] + Key string + DefaultOptions options[string, string] + CallOptions []Option[string, string] + Metrics Metrics + Result *Item[string, string] + ExpectedNumberOfAllocations int }{ "Get without loader when item is not found": { Key: notFoundKey, @@ -551,7 +552,8 @@ func Test_Cache_Get(t *testing.T) { Metrics: Metrics{ Misses: 1, }, - Result: &Item[string, string]{key: "test"}, + Result: &Item[string, string]{key: "test"}, + ExpectedNumberOfAllocations: 1, // The loader function returns a heap allocated item. If this item was pre-allocated, the number of allocations would be 0. }, "Get with default loader that returns nil value when item is not found": { Key: notFoundKey, @@ -579,7 +581,8 @@ func Test_Cache_Get(t *testing.T) { Metrics: Metrics{ Misses: 1, }, - Result: &Item[string, string]{key: "hello"}, + Result: &Item[string, string]{key: "hello"}, + ExpectedNumberOfAllocations: 1, // The loader function returns a heap allocated item. If this item was pre-allocated, the number of allocations would be 0. }, "Get with call loader that returns nil value when item is not found": { Key: notFoundKey, @@ -633,7 +636,10 @@ func Test_Cache_Get(t *testing.T) { oldExpiresAt := cache.items.values[foundKey].Value.(*Item[string, string]).expiresAt cache.options = c.DefaultOptions - res := cache.Get(c.Key, c.CallOptions...) + var res *Item[string, string] + assert.Equal(t, c.ExpectedNumberOfAllocations, allocsForSingleRun(func() { + res = cache.Get(c.Key, c.CallOptions...) + })) if c.Key == foundKey { c.Result = cache.items.values[foundKey].Value.(*Item[string, string]) @@ -646,7 +652,7 @@ func Test_Cache_Get(t *testing.T) { return } - applyOptions(&c.DefaultOptions, c.CallOptions...) + c.DefaultOptions = applyOptions(c.DefaultOptions, c.CallOptions...) if c.DefaultOptions.disableTouchOnHit { assert.Equal(t, oldExpiresAt, res.expiresAt) @@ -1302,3 +1308,18 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { } } } + +func allocsForSingleRun(f func()) (numAllocs int) { + // `testing.AllocsPerRun` "warms up" the function for a single run before + // measuring allocations, so we need do nothing on the first run. + firstRun := false + numAllocs = int(testing.AllocsPerRun(1, func() { + if !firstRun { + firstRun = true + return + } + + f() + })) + return +} diff --git a/options.go b/options.go index 6607a09..3391e7c 100644 --- a/options.go +++ b/options.go @@ -4,15 +4,15 @@ import "time" // Option sets a specific cache option. type Option[K comparable, V any] interface { - apply(opts *options[K, V]) + apply(opts options[K, V]) options[K, V] } // optionFunc wraps a function and implements the Option interface. -type optionFunc[K comparable, V any] func(*options[K, V]) +type optionFunc[K comparable, V any] func(options[K, V]) options[K, V] // apply calls the wrapped function. -func (fn optionFunc[K, V]) apply(opts *options[K, V]) { - fn(opts) +func (fn optionFunc[K, V]) apply(opts options[K, V]) options[K, V] { + return fn(opts) } // CostFunc is used to calculate the cost of the key and the item to be @@ -29,26 +29,29 @@ type options[K comparable, V any] struct { itemOpts []itemOption[K, V] } -// applyOptions applies the provided option values to the option struct. -func applyOptions[K comparable, V any](v *options[K, V], opts ...Option[K, V]) { +// applyOptions applies the provided option values to the option struct and returns the modified option struct. +func applyOptions[K comparable, V any](v options[K, V], opts ...Option[K, V]) options[K, V] { for i := range opts { - opts[i].apply(v) + v = opts[i].apply(v) } + return v } // WithCapacity sets the maximum capacity of the cache. // It has no effect when used with Get(). func WithCapacity[K comparable, V any](c uint64) Option[K, V] { - return optionFunc[K, V](func(opts *options[K, V]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.capacity = c + return opts }) } // WithTTL sets the TTL of the cache. // It has no effect when used with Get(). func WithTTL[K comparable, V any](ttl time.Duration) Option[K, V] { - return optionFunc[K, V](func(opts *options[K, V]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.ttl = ttl + return opts }) } @@ -56,8 +59,9 @@ func WithTTL[K comparable, V any](ttl time.Duration) Option[K, V] { // If version tracking is disabled, the version is always -1. // 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]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.itemOpts = append(opts.itemOpts, withVersionTracking[K, V](enable)) + return opts }) } @@ -65,8 +69,9 @@ func WithVersion[K comparable, V any](enable bool) Option[K, V] { // When passing into Get(), it sets an ephemeral loader that // is used instead of the cache's default one. func WithLoader[K comparable, V any](l Loader[K, V]) Option[K, V] { - return optionFunc[K, V](func(opts *options[K, V]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.loader = l + return opts }) } @@ -76,8 +81,9 @@ func WithLoader[K comparable, V any](l Loader[K, V]) Option[K, V] { // When used with Get(), it overrides the default value of the // cache. func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { - return optionFunc[K, V](func(opts *options[K, V]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.disableTouchOnHit = true + return opts }) } @@ -85,9 +91,10 @@ func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] { // The actual cost calculation for each inserted item happens by making use of the // callback CostFunc. func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] { - return optionFunc[K, V](func(opts *options[K, V]) { + return optionFunc[K, V](func(opts options[K, V]) options[K, V] { opts.maxCost = s opts.itemOpts = append(opts.itemOpts, withCostFunc[K, V](callback)) + return opts }) } diff --git a/options_test.go b/options_test.go index 478dc40..bb3db71 100644 --- a/options_test.go +++ b/options_test.go @@ -13,9 +13,10 @@ func Test_optionFunc_apply(t *testing.T) { var called bool - optionFunc[string, string](func(_ *options[string, string]) { + optionFunc[string, string](func(opts options[string, string]) options[string, string] { called = true - }).apply(nil) + return opts + }).apply(options[string, string]{}) assert.True(t, called) } @@ -24,7 +25,7 @@ func Test_applyOptions(t *testing.T) { var opts options[string, string] - applyOptions(&opts, + opts = applyOptions(opts, WithCapacity[string, string](12), WithTTL[string, string](time.Hour), ) @@ -38,7 +39,7 @@ func Test_WithCapacity(t *testing.T) { var opts options[string, string] - WithCapacity[string, string](12).apply(&opts) + opts = WithCapacity[string, string](12).apply(opts) assert.Equal(t, uint64(12), opts.capacity) } @@ -47,7 +48,7 @@ func Test_WithTTL(t *testing.T) { var opts options[string, string] - WithTTL[string, string](time.Hour).apply(&opts) + opts = WithTTL[string, string](time.Hour).apply(opts) assert.Equal(t, time.Hour, opts.ttl) } @@ -57,13 +58,13 @@ func Test_WithVersion(t *testing.T) { var opts options[string, string] var item Item[string, string] - WithVersion[string, string](true).apply(&opts) + opts = WithVersion[string, string](true).apply(opts) assert.Len(t, opts.itemOpts, 1) opts.itemOpts[0].apply(&item) assert.Equal(t, int64(0), item.version) opts.itemOpts = []itemOption[string, string]{} - WithVersion[string, string](false).apply(&opts) + opts = WithVersion[string, string](false).apply(opts) assert.Len(t, opts.itemOpts, 1) opts.itemOpts[0].apply(&item) assert.Equal(t, int64(-1), item.version) @@ -77,7 +78,7 @@ func Test_WithLoader(t *testing.T) { l := LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { return nil }) - WithLoader[string, string](l).apply(&opts) + opts = WithLoader[string, string](l).apply(opts) assert.NotNil(t, opts.loader) } @@ -86,7 +87,7 @@ func Test_WithDisableTouchOnHit(t *testing.T) { var opts options[string, string] - WithDisableTouchOnHit[string, string]().apply(&opts) + opts = WithDisableTouchOnHit[string, string]().apply(opts) assert.True(t, opts.disableTouchOnHit) } @@ -96,7 +97,7 @@ func Test_WithMaxCost(t *testing.T) { var opts options[string, string] var item Item[string, string] - WithMaxCost[string, string](1024, func(item *Item[string, string]) uint64 { return 1 }).apply(&opts) + opts = WithMaxCost[string, string](1024, func(item *Item[string, string]) uint64 { return 1 }).apply(opts) assert.Equal(t, uint64(1024), opts.maxCost) assert.Len(t, opts.itemOpts, 1)