diff --git a/cache.go b/cache.go index 3005481..6969c5b 100644 --- a/cache.go +++ b/cache.go @@ -319,13 +319,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) { +func (c *Cache[K, V]) delete(key K, version *int64) bool { elem := c.items.values[key] - if elem == nil { - return + if elem == nil || (version != nil && elem.Value.(*Item[K, V]).version != *version) { + return false } c.evict(EvictionReasonDeleted, elem) + + return true } // Set creates a new item from the provided key and value, adds @@ -357,7 +359,20 @@ 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. +// 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() + + return c.delete(key, &version) } // Has checks whether the key exists in the cache. @@ -422,7 +437,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 6c4b338..0cb2e88 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), ) @@ -687,6 +687,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(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++ + } + cache.events.eviction.fns[2] = cache.events.eviction.fns[1] + + // not found + assert.False(t, cache.OptimisticDelete("1234", 0)) + assert.Zero(t, fnsCalls) + assert.Len(t, cache.items.values, 4) + + // invalid version + 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 + 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") +} + func Test_Cache_Has(t *testing.T) { cache := prepCache(0, time.Hour, "1") addToCache(cache, time.Nanosecond, "2") @@ -1267,15 +1295,19 @@ 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)) + c.options.itemOpts = append( + c.options.itemOpts, + withVersionTracking[string, string](true), + ) if maxCost != 0 { c.options.maxCost = maxCost - c.options.itemOpts = append(c.options.itemOpts, - withCostFunc[string, string](func(item *Item[string, string]) uint64 { + c.options.itemOpts = append( + c.options.itemOpts, + withCostFunc(func(item *Item[string, string]) uint64 { return uint64(len(item.value)) - })) + }), + ) } c.items.values = make(map[string]*list.Element)