Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
44 changes: 38 additions & 6 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
Loading