[bugfix] update go-cache to v3.3.3 (#1778)

* update go-cache to v3.3.1

Signed-off-by: kim <grufwub@gmail.com>

* bump go-cache again

Signed-off-by: kim <grufwub@gmail.com>

* remove accidentally comitted build-script changes

Signed-off-by: kim <grufwub@gmail.com>

* now v3.3.3 ...

Signed-off-by: kim <grufwub@gmail.com>

---------

Signed-off-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2023-05-14 14:17:03 +01:00 committed by GitHub
parent 514eb8c83e
commit 2b7c815ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 466 additions and 240 deletions

2
go.mod
View file

@ -5,7 +5,7 @@ go 1.20
require (
codeberg.org/gruf/go-bytesize v1.0.2
codeberg.org/gruf/go-byteutil v1.1.2
codeberg.org/gruf/go-cache/v3 v3.3.0
codeberg.org/gruf/go-cache/v3 v3.3.3
codeberg.org/gruf/go-debug v1.3.0
codeberg.org/gruf/go-errors/v2 v2.2.0
codeberg.org/gruf/go-fastcopy v1.1.2

4
go.sum
View file

@ -49,8 +49,8 @@ codeberg.org/gruf/go-bytesize v1.0.2/go.mod h1:n/GU8HzL9f3UNp/mUKyr1qVmTlj7+xacp
codeberg.org/gruf/go-byteutil v1.0.0/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
codeberg.org/gruf/go-byteutil v1.1.2 h1:TQLZtTxTNca9xEfDIndmo7nBYxeS94nrv/9DS3Nk5Tw=
codeberg.org/gruf/go-byteutil v1.1.2/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
codeberg.org/gruf/go-cache/v3 v3.3.0 h1:Bor75j4MYJIDqH22/aQvmwA7hMqBOzDOWdSQz25Lq+8=
codeberg.org/gruf/go-cache/v3 v3.3.0/go.mod h1:pTeVPEb9DshXUkd8Dg76UcsLpU6EC/tXQ2qb+JrmxEc=
codeberg.org/gruf/go-cache/v3 v3.3.3 h1:CzOFg6JV+typ8Jst1rrYDbZjxEV7bUxKggkbfN5Y79o=
codeberg.org/gruf/go-cache/v3 v3.3.3/go.mod h1:pTeVPEb9DshXUkd8Dg76UcsLpU6EC/tXQ2qb+JrmxEc=
codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg=
codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4=

View file

@ -15,10 +15,10 @@ type Cache[Key comparable, Value any] interface {
Stop() bool
// SetEvictionCallback sets the eviction callback to the provided hook.
SetEvictionCallback(hook func(*ttlcache.Entry[Key, Value]))
SetEvictionCallback(hook func(Key, Value))
// SetInvalidateCallback sets the invalidate callback to the provided hook.
SetInvalidateCallback(hook func(*ttlcache.Entry[Key, Value]))
SetInvalidateCallback(hook func(Key, Value))
// SetTTL sets the cache item TTL. Update can be specified to force updates of existing items in the cache, this will simply add the change in TTL to their current expiry time.
SetTTL(ttl time.Duration, update bool)
@ -44,6 +44,9 @@ type Cache[Key comparable, Value any] interface {
// Invalidate deletes a value from the cache, calling the invalidate callback.
Invalidate(key Key) bool
// InvalidateAll is equivalent to multiple Invalidate calls.
InvalidateAll(keys ...Key) bool
// Clear empties the cache, calling the invalidate callback on each entry.
Clear()

View file

@ -97,20 +97,22 @@ func (c *Cache[Value]) SetEvictionCallback(hook func(Value)) {
// Ensure non-nil hook.
hook = func(Value) {}
}
c.cache.SetEvictionCallback(func(item *ttl.Entry[int64, result[Value]]) {
for _, key := range item.Value.Keys {
c.cache.SetEvictionCallback(func(pkey int64, res result[Value]) {
c.cache.Lock()
for _, key := range res.Keys {
// Delete key->pkey lookup
pkeys := key.info.pkeys
delete(pkeys, key.key)
}
c.cache.Unlock()
if item.Value.Error != nil {
if res.Error != nil {
// Skip error hooks
return
}
// Call user hook.
hook(item.Value.Value)
hook(res.Value)
})
}
@ -121,20 +123,22 @@ func (c *Cache[Value]) SetInvalidateCallback(hook func(Value)) {
hook = func(Value) {}
} // store hook.
c.invalid = hook
c.cache.SetInvalidateCallback(func(item *ttl.Entry[int64, result[Value]]) {
for _, key := range item.Value.Keys {
c.cache.SetInvalidateCallback(func(pkey int64, res result[Value]) {
c.cache.Lock()
for _, key := range res.Keys {
// Delete key->pkey lookup
pkeys := key.info.pkeys
delete(pkeys, key.key)
}
c.cache.Unlock()
if item.Value.Error != nil {
if res.Error != nil {
// Skip error hooks
return
}
// Call user hook.
hook(item.Value.Value)
hook(res.Value)
})
}
@ -178,10 +182,18 @@ func (c *Cache[Value]) Load(lookup string, load func() (Value, error), keyParts
pkeys := keyInfo.pkeys[ckey]
if ok = (len(pkeys) > 0); ok {
var entry *ttl.Entry[int64, result[Value]]
// Fetch the result for primary key
entry, _ := c.cache.Cache.Get(pkeys[0])
entry, ok = c.cache.Cache.Get(pkeys[0])
if ok {
// Since the invalidation / eviction hooks acquire a mutex
// lock separately, and only at this point are the pkeys
// updated, there is a chance that a primary key may return
// no matching entry. Hence we have to check for it here.
res = entry.Value
}
}
// Done with lock
c.cache.Unlock()
@ -213,12 +225,19 @@ func (c *Cache[Value]) Load(lookup string, load func() (Value, error), keyParts
res.Keys = c.lookups.generate(res.Value)
}
var evict func()
// Acquire cache lock.
c.cache.Lock()
defer c.cache.Unlock()
defer func() {
c.cache.Unlock()
if evict != nil {
evict()
}
}()
// Cache result
c.store(res)
// Store result in cache.
evict = c.store(res)
}
// Catch and return error
@ -244,12 +263,19 @@ func (c *Cache[Value]) Store(value Value, store func() error) error {
Error: nil,
}
var evict func()
// Acquire cache lock.
c.cache.Lock()
defer c.cache.Unlock()
defer func() {
c.cache.Unlock()
if evict != nil {
evict()
}
}()
// Cache result
c.store(result)
// Store result in cache.
evict = c.store(result)
// Call invalidate.
c.invalid(value)
@ -278,10 +304,18 @@ func (c *Cache[Value]) Has(lookup string, keyParts ...any) bool {
pkeys := keyInfo.pkeys[ckey]
if ok = (len(pkeys) > 0); ok {
var entry *ttl.Entry[int64, result[Value]]
// Fetch the result for primary key
entry, _ := c.cache.Cache.Get(pkeys[0])
entry, ok = c.cache.Cache.Get(pkeys[0])
if ok {
// Since the invalidation / eviction hooks acquire a mutex
// lock separately, and only at this point are the pkeys
// updated, there is a chance that a primary key may return
// no matching entry. Hence we have to check for it here.
res = entry.Value
}
}
// Done with lock
c.cache.Unlock()
@ -301,19 +335,18 @@ func (c *Cache[Value]) Invalidate(lookup string, keyParts ...any) {
// Look for primary key for cache key
c.cache.Lock()
pkeys := keyInfo.pkeys[ckey]
delete(keyInfo.pkeys, ckey)
c.cache.Unlock()
for _, pkey := range pkeys {
// Invalidate each primary key
c.cache.Invalidate(pkey)
}
// Invalidate all primary keys.
c.cache.InvalidateAll(pkeys...)
}
// Clear empties the cache, calling the invalidate callback.
func (c *Cache[Value]) Clear() { c.cache.Clear() }
// store will cache this result under all of its required cache keys.
func (c *Cache[Value]) store(res result[Value]) {
func (c *Cache[Value]) store(res result[Value]) (evict func()) {
// Get primary key
pnext := c.next
c.next++
@ -341,7 +374,7 @@ func (c *Cache[Value]) store(res result[Value]) {
}
}
// Drop these keys.
// Drop existing.
pkeys = pkeys[:0]
}
@ -356,8 +389,10 @@ func (c *Cache[Value]) store(res result[Value]) {
Key: pnext,
Value: res,
}, func(_ int64, item *ttl.Entry[int64, result[Value]]) {
c.cache.Evict(item)
evict = func() { c.cache.Evict(item.Key, item.Value) }
})
return evict
}
type result[Value any] struct {

View file

@ -20,10 +20,10 @@ type Cache[Key comparable, Value any] struct {
TTL time.Duration
// Evict is the hook that is called when an item is evicted from the cache.
Evict func(*Entry[Key, Value])
Evict func(Key, Value)
// Invalid is the hook that is called when an item's data in the cache is invalidated, includes Add/Set.
Invalid func(*Entry[Key, Value])
Invalid func(Key, Value)
// Cache is the underlying hashmap used for this cache.
Cache maps.LRUMap[Key, *Entry[Key, Value]]
@ -97,14 +97,17 @@ func (c *Cache[K, V]) Stop() (ok bool) {
// Sweep attempts to evict expired items (with callback!) from cache.
func (c *Cache[K, V]) Sweep(now time.Time) {
var after int
var (
// evicted key-values.
kvs []kv[K, V]
// Sweep within lock
c.Lock()
defer c.Unlock()
// hook func ptrs.
evict func(K, V)
)
c.locked(func() {
// Sentinel value
after = -1
after := -1
// The cache will be ordered by expiry date, we iterate until we reach the index of
// the youngest item that hsa expired, as all succeeding items will also be expired.
@ -112,11 +115,12 @@ func (c *Cache[K, V]) Sweep(now time.Time) {
if now.After(item.Expiry) {
after = i
// All older than this (including) can be dropped
// evict all older items
// than this (inclusive)
return false
}
// Continue looping
// cont. loop.
return true
})
@ -125,38 +129,34 @@ func (c *Cache[K, V]) Sweep(now time.Time) {
return
}
// Truncate items, calling eviction hook
c.truncate(c.Cache.Len()-after, c.Evict)
// Set hook func ptr.
evict = c.Evict
// Truncate determined size.
sz := c.Cache.Len() - after
kvs = c.truncate(sz, evict)
})
if evict != nil {
for x := range kvs {
// Pass to eviction hook.
evict(kvs[x].K, kvs[x].V)
}
}
}
// SetEvictionCallback: implements cache.Cache's SetEvictionCallback().
func (c *Cache[K, V]) SetEvictionCallback(hook func(*Entry[K, V])) {
// Ensure non-nil hook
if hook == nil {
hook = func(*Entry[K, V]) {}
}
// Update within lock
c.Lock()
defer c.Unlock()
// Update hook
func (c *Cache[K, V]) SetEvictionCallback(hook func(K, V)) {
c.locked(func() {
c.Evict = hook
})
}
// SetInvalidateCallback: implements cache.Cache's SetInvalidateCallback().
func (c *Cache[K, V]) SetInvalidateCallback(hook func(*Entry[K, V])) {
// Ensure non-nil hook
if hook == nil {
hook = func(*Entry[K, V]) {}
}
// Update within lock
c.Lock()
defer c.Unlock()
// Update hook
func (c *Cache[K, V]) SetInvalidateCallback(hook func(K, V)) {
c.locked(func() {
c.Invalid = hook
})
}
// SetTTL: implements cache.Cache's SetTTL().
@ -165,10 +165,7 @@ func (c *Cache[K, V]) SetTTL(ttl time.Duration, update bool) {
panic("ttl must be greater than zero")
}
// Update within lock
c.Lock()
defer c.Unlock()
c.locked(func() {
// Set updated TTL
diff := ttl - c.TTL
c.TTL = ttl
@ -179,230 +176,406 @@ func (c *Cache[K, V]) SetTTL(ttl time.Duration, update bool) {
item.Expiry = item.Expiry.Add(diff)
})
}
})
}
// Get: implements cache.Cache's Get().
func (c *Cache[K, V]) Get(key K) (V, bool) {
// Read within lock
c.Lock()
defer c.Unlock()
var (
// did exist in cache?
ok bool
// cached value.
v V
)
c.locked(func() {
var item *Entry[K, V]
// Check for item in cache
item, ok := c.Cache.Get(key)
item, ok = c.Cache.Get(key)
if !ok {
var value V
return value, false
return
}
// Update item expiry and return
// Update fetched item's expiry
item.Expiry = time.Now().Add(c.TTL)
return item.Value, true
// Set value.
v = item.Value
})
return v, ok
}
// Add: implements cache.Cache's Add().
func (c *Cache[K, V]) Add(key K, value V) bool {
// Write within lock
c.Lock()
defer c.Unlock()
var (
// did exist in cache?
ok bool
// Check if already exists
item, ok := c.Cache.Get(key)
// was entry evicted?
ev bool
// evicted key values.
evcK K
evcV V
// hook func ptrs.
evict func(K, V)
)
c.locked(func() {
// Check if in cache.
ok = c.Cache.Has(key)
if ok {
return false
return
}
// Alloc new item
item = c.alloc()
item.Key = key
item.Value = value
item.Expiry = time.Now().Add(c.TTL)
// Alloc new entry.
new := c.alloc()
new.Expiry = time.Now().Add(c.TTL)
new.Key = key
new.Value = value
var hook func(K, *Entry[K, V])
// Add new entry to cache and catched any evicted item.
c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
evcK = item.Key
evcV = item.Value
ev = true
c.free(item)
})
if c.Evict != nil {
// Pass evicted entry to user hook
hook = func(_ K, item *Entry[K, V]) {
c.Evict(item)
}
// Set hook func ptr.
evict = c.Evict
})
if ev && evict != nil {
// Pass to eviction hook.
evict(evcK, evcV)
}
// Place new item in the map with hook
c.Cache.SetWithHook(key, item, hook)
if c.Invalid != nil {
// invalidate old
c.Invalid(item)
}
return true
return !ok
}
// Set: implements cache.Cache's Set().
func (c *Cache[K, V]) Set(key K, value V) {
// Write within lock
c.Lock()
defer c.Unlock()
var (
// did exist in cache?
ok bool
// Check if already exists
item, ok := c.Cache.Get(key)
// was entry evicted?
ev bool
if !ok {
var hook func(K, *Entry[K, V])
// old value.
oldV V
// Allocate new item
item = c.alloc()
item.Key = key
// evicted key values.
evcK K
evcV V
if c.Evict != nil {
// Pass evicted entry to user hook
hook = func(_ K, item *Entry[K, V]) {
c.Evict(item)
}
}
// hook func ptrs.
invalid func(K, V)
evict func(K, V)
)
// Place new item in the map with hook
c.Cache.SetWithHook(key, item, hook)
}
c.locked(func() {
var item *Entry[K, V]
if c.Invalid != nil {
// invalidate old
c.Invalid(item)
}
// Check for item in cache
item, ok = c.Cache.Get(key)
// Update the item value + expiry
if ok {
// Set old value.
oldV = item.Value
// Update the existing item.
item.Expiry = time.Now().Add(c.TTL)
item.Value = value
} else {
// Alloc new entry.
new := c.alloc()
new.Expiry = time.Now().Add(c.TTL)
new.Key = key
new.Value = value
// Add new entry to cache and catched any evicted item.
c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
evcK = item.Key
evcV = item.Value
ev = true
c.free(item)
})
}
// Set hook func ptrs.
invalid = c.Invalid
evict = c.Evict
})
if ok && invalid != nil {
// Pass to invalidate hook.
invalid(key, oldV)
}
if ev && evict != nil {
// Pass to eviction hook.
evict(evcK, evcV)
}
}
// CAS: implements cache.Cache's CAS().
func (c *Cache[K, V]) CAS(key K, old V, new V, cmp func(V, V) bool) bool {
// CAS within lock
c.Lock()
defer c.Unlock()
var (
// did exist in cache?
ok bool
// swapped value.
oldV V
// hook func ptrs.
invalid func(K, V)
)
c.locked(func() {
var item *Entry[K, V]
// Check for item in cache
item, ok := c.Cache.Get(key)
if !ok || !cmp(item.Value, old) {
return false
item, ok = c.Cache.Get(key)
if !ok {
return
}
if c.Invalid != nil {
// invalidate old
c.Invalid(item)
// Perform the comparison
if !cmp(old, item.Value) {
return
}
// Update item + Expiry
item.Value = new
// Set old value.
oldV = item.Value
// Update value + expiry.
item.Expiry = time.Now().Add(c.TTL)
item.Value = new
// Set hook func ptr.
invalid = c.Invalid
})
if ok && invalid != nil {
// Pass to invalidate hook.
invalid(key, oldV)
}
return ok
}
// Swap: implements cache.Cache's Swap().
func (c *Cache[K, V]) Swap(key K, swp V) V {
// Swap within lock
c.Lock()
defer c.Unlock()
var (
// did exist in cache?
ok bool
// swapped value.
oldV V
// hook func ptrs.
invalid func(K, V)
)
c.locked(func() {
var item *Entry[K, V]
// Check for item in cache
item, ok := c.Cache.Get(key)
item, ok = c.Cache.Get(key)
if !ok {
var value V
return value
return
}
if c.Invalid != nil {
// invalidate old
c.Invalid(item)
}
// Set old value.
oldV = item.Value
old := item.Value
// update item + Expiry
item.Value = swp
// Update value + expiry.
item.Expiry = time.Now().Add(c.TTL)
item.Value = swp
return old
// Set hook func ptr.
invalid = c.Invalid
})
if ok && invalid != nil {
// Pass to invalidate hook.
invalid(key, oldV)
}
return oldV
}
// Has: implements cache.Cache's Has().
func (c *Cache[K, V]) Has(key K) bool {
c.Lock()
ok := c.Cache.Has(key)
c.Unlock()
return ok
func (c *Cache[K, V]) Has(key K) (ok bool) {
c.locked(func() {
ok = c.Cache.Has(key)
})
return
}
// Invalidate: implements cache.Cache's Invalidate().
func (c *Cache[K, V]) Invalidate(key K) bool {
// Delete within lock
c.Lock()
defer c.Unlock()
func (c *Cache[K, V]) Invalidate(key K) (ok bool) {
var (
// old value.
oldV V
// Check if we have item with key
item, ok := c.Cache.Get(key)
// hook func ptrs.
invalid func(K, V)
)
c.locked(func() {
var item *Entry[K, V]
// Check for item in cache
item, ok = c.Cache.Get(key)
if !ok {
return false
return
}
// Set old value.
oldV = item.Value
// Remove from cache map
_ = c.Cache.Delete(key)
if c.Invalid != nil {
// Invalidate item
c.Invalid(item)
}
// Return item to pool
// Free entry
c.free(item)
return true
// Set hook func ptrs.
invalid = c.Invalid
})
if ok && invalid != nil {
// Pass to invalidate hook.
invalid(key, oldV)
}
return
}
// InvalidateAll: implements cache.Cache's InvalidateAll().
func (c *Cache[K, V]) InvalidateAll(keys ...K) (ok bool) {
var (
// invalidated kvs.
kvs []kv[K, V]
// hook func ptrs.
invalid func(K, V)
)
// Allocate a slice for invalidated.
kvs = make([]kv[K, V], 0, len(keys))
c.locked(func() {
for _, key := range keys {
var item *Entry[K, V]
// Check for item in cache
item, ok = c.Cache.Get(key)
if !ok {
return
}
// Append this old value to slice
kvs = append(kvs, kv[K, V]{
K: key,
V: item.Value,
})
// Remove from cache map
_ = c.Cache.Delete(key)
// Free entry
c.free(item)
}
// Set hook func ptrs.
invalid = c.Invalid
})
if invalid != nil {
for x := range kvs {
// Pass to invalidate hook.
invalid(kvs[x].K, kvs[x].V)
}
}
return
}
// Clear: implements cache.Cache's Clear().
func (c *Cache[K, V]) Clear() {
c.Lock()
defer c.Unlock()
c.truncate(c.Cache.Len(), c.Invalid)
var (
// deleted key-values.
kvs []kv[K, V]
// hook func ptrs.
invalid func(K, V)
)
c.locked(func() {
// Set hook func ptr.
invalid = c.Invalid
// Truncate the entire cache length.
kvs = c.truncate(c.Cache.Len(), invalid)
})
if invalid != nil {
for x := range kvs {
// Pass to invalidate hook.
invalid(kvs[x].K, kvs[x].V)
}
}
}
// Len: implements cache.Cache's Len().
func (c *Cache[K, V]) Len() int {
c.Lock()
l := c.Cache.Len()
c.Unlock()
return l
}
// Cap: implements cache.Cache's Cap().
func (c *Cache[K, V]) Cap() int {
c.Lock()
l := c.Cache.Cap()
c.Unlock()
return l
}
// truncate will call Cache.Truncate(sz), and if provided a hook will temporarily store deleted items before passing them to the hook. This is required in order to prevent cache writes during .Truncate().
func (c *Cache[K, V]) truncate(sz int, hook func(*Entry[K, V])) {
if hook == nil {
// No hook was provided, we can simply truncate and free items immediately.
c.Cache.Truncate(sz, func(_ K, item *Entry[K, V]) { c.free(item) })
func (c *Cache[K, V]) Len() (l int) {
c.locked(func() { l = c.Cache.Len() })
return
}
// Store list of deleted items for later callbacks
deleted := make([]*Entry[K, V], 0, sz)
// Cap: implements cache.Cache's Cap().
func (c *Cache[K, V]) Cap() (l int) {
c.locked(func() { l = c.Cache.Cap() })
return
}
func (c *Cache[K, V]) locked(fn func()) {
c.Lock()
fn()
c.Unlock()
}
// truncate will truncate the cache by given size, returning deleted items.
func (c *Cache[K, V]) truncate(sz int, hook func(K, V)) []kv[K, V] {
if hook == nil {
// No hook to execute, simply free all truncated entries.
c.Cache.Truncate(sz, func(_ K, e *Entry[K, V]) { c.free(e) })
return nil
}
// Allocate a slice for deleted k-v pairs.
deleted := make([]kv[K, V], 0, sz)
// Truncate and store list of deleted items
c.Cache.Truncate(sz, func(_ K, item *Entry[K, V]) {
deleted = append(deleted, item)
// Store key-value pair for later access.
deleted = append(deleted, kv[K, V]{
K: item.Key,
V: item.Value,
})
// Pass each deleted to hook, then free
for _, item := range deleted {
hook(item)
// Free entry.
c.free(item)
}
})
return deleted
}
// alloc will acquire cache entry from pool, or allocate new.
@ -416,14 +589,29 @@ func (c *Cache[K, V]) alloc() *Entry[K, V] {
return e
}
// clone allocates a new Entry and copies all info from passed Entry.
func (c *Cache[K, V]) clone(e *Entry[K, V]) *Entry[K, V] {
e2 := c.alloc()
e2.Key = e.Key
e2.Value = e.Value
e2.Expiry = e.Expiry
return e2
}
// free will reset entry fields and place back in pool.
func (c *Cache[K, V]) free(e *Entry[K, V]) {
var (
zk K
zv V
zt time.Time
)
e.Expiry = zt
e.Key = zk
e.Value = zv
e.Expiry = time.Time{}
c.pool = append(c.pool, e)
}
type kv[K comparable, V any] struct {
K K
V V
}

2
vendor/modules.txt vendored
View file

@ -13,7 +13,7 @@ codeberg.org/gruf/go-bytesize
# codeberg.org/gruf/go-byteutil v1.1.2
## explicit; go 1.16
codeberg.org/gruf/go-byteutil
# codeberg.org/gruf/go-cache/v3 v3.3.0
# codeberg.org/gruf/go-cache/v3 v3.3.3
## explicit; go 1.19
codeberg.org/gruf/go-cache/v3
codeberg.org/gruf/go-cache/v3/result