From 079462e88cabfbac9b7d21cfcf1ce6e45f9a122e Mon Sep 17 00:00:00 2001 From: david Date: Fri, 7 Mar 2025 15:09:38 +0200 Subject: [PATCH 1/2] Add OptimisticDelete method --- cache.go | 20 ++++++++++++++++---- cache_test.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cache.go b/cache.go index 1ad3afb..6f22fb6 100644 --- a/cache.go +++ b/cache.go @@ -293,9 +293,9 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { // The method is no-op if the item is not found. // Not safe for concurrent use by multiple goroutines without additional // locking. -func (c *Cache[K, V]) delete(key K) { +func (c *Cache[K, V]) delete(key K, version *int64) { elem := c.items.values[key] - if elem == nil { + if elem == nil || (version != nil && elem.Value.(*Item[K, V]).version != *version) { return } @@ -331,7 +331,19 @@ func (c *Cache[K, V]) Delete(key K) { c.items.mu.Lock() defer c.items.mu.Unlock() - c.delete(key) + c.delete(key, nil) +} + +// OptimisticDelete deletes an item from the cache if the +// provided version matches with the item version. If the +// item associated with the key is not found, the method is no-op. +// In order to use this method and item versions, the cache +// should be initialized using the WithVersion option. +func (c *Cache[K, V]) OptimisticDelete(key K, version int64) { + c.items.mu.Lock() + defer c.items.mu.Unlock() + + c.delete(key, &version) } // Has checks whether the key exists in the cache. @@ -396,7 +408,7 @@ func (c *Cache[K, V]) GetAndDelete(key K, opts ...Option[K, V]) (*Item[K, V], bo return nil, false } - c.delete(key) + c.delete(key, nil) c.items.mu.Unlock() return elem, true diff --git a/cache_test.go b/cache_test.go index 0c91512..221404e 100644 --- a/cache_test.go +++ b/cache_test.go @@ -19,7 +19,7 @@ func TestMain(m *testing.M) { } func Test_New(t *testing.T) { - c := New[string, string]( + c := New( WithTTL[string, string](time.Hour), WithCapacity[string, string](1), ) @@ -621,6 +621,34 @@ func Test_Cache_Delete(t *testing.T) { assert.NotContains(t, cache.items.values, "1") } +func Test_Cache_OptimisticDelete(t *testing.T) { + var fnsCalls int + + cache := prepCache(time.Hour, "1", "2", "3", "4") + cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { + assert.Equal(t, EvictionReasonDeleted, r) + fnsCalls++ + } + cache.events.eviction.fns[2] = cache.events.eviction.fns[1] + + // not found + cache.OptimisticDelete("1234", 0) + assert.Zero(t, fnsCalls) + assert.Len(t, cache.items.values, 4) + + // invalid version + cache.OptimisticDelete("1", 1) + assert.Zero(t, fnsCalls) + assert.Len(t, cache.items.values, 4) + assert.Contains(t, cache.items.values, "1") + + // success + cache.OptimisticDelete("1", 0) + assert.Equal(t, 2, fnsCalls) + assert.Len(t, cache.items.values, 3) + assert.NotContains(t, cache.items.values, "1") +} + func Test_Cache_Has(t *testing.T) { cc := map[string]struct { keys []string @@ -1205,7 +1233,7 @@ func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { key, fmt.Sprint("value of", key), ttl+time.Duration(i)*time.Minute, - false, + true, ) elem := c.items.lru.PushFront(item) c.items.values[key] = elem From 62e84c4eab22945213dc2e8bc3ec392fb042e4d0 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 18 Apr 2025 22:19:40 +0300 Subject: [PATCH 2/2] return boolean indicating a hit --- cache.go | 11 +++++++---- cache_test.go | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/cache.go b/cache.go index 6f22fb6..d0c7f6c 100644 --- a/cache.go +++ b/cache.go @@ -293,13 +293,15 @@ func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { // The method is no-op if the item is not found. // Not safe for concurrent use by multiple goroutines without additional // locking. -func (c *Cache[K, V]) delete(key K, version *int64) { +func (c *Cache[K, V]) delete(key K, version *int64) bool { elem := c.items.values[key] if elem == nil || (version != nil && elem.Value.(*Item[K, V]).version != *version) { - return + return false } c.evict(EvictionReasonDeleted, elem) + + return true } // Set creates a new item from the provided key and value, adds @@ -339,11 +341,12 @@ func (c *Cache[K, V]) Delete(key K) { // item associated with the key is not found, the method is no-op. // In order to use this method and item versions, the cache // should be initialized using the WithVersion option. -func (c *Cache[K, V]) OptimisticDelete(key K, version int64) { +// The return value indicates whether the item was matched and deleted. +func (c *Cache[K, V]) OptimisticDelete(key K, version int64) bool { c.items.mu.Lock() defer c.items.mu.Unlock() - c.delete(key, &version) + return c.delete(key, &version) } // Has checks whether the key exists in the cache. diff --git a/cache_test.go b/cache_test.go index 221404e..f539ab9 100644 --- a/cache_test.go +++ b/cache_test.go @@ -632,18 +632,18 @@ func Test_Cache_OptimisticDelete(t *testing.T) { cache.events.eviction.fns[2] = cache.events.eviction.fns[1] // not found - cache.OptimisticDelete("1234", 0) + assert.False(t, cache.OptimisticDelete("1234", 0)) assert.Zero(t, fnsCalls) assert.Len(t, cache.items.values, 4) // invalid version - cache.OptimisticDelete("1", 1) + assert.False(t, cache.OptimisticDelete("1", 1)) assert.Zero(t, fnsCalls) assert.Len(t, cache.items.values, 4) assert.Contains(t, cache.items.values, "1") // success - cache.OptimisticDelete("1", 0) + assert.True(t, cache.OptimisticDelete("1", 0)) assert.Equal(t, 2, fnsCalls) assert.Len(t, cache.items.values, 3) assert.NotContains(t, cache.items.values, "1")