From 79294999fbf06bfb5b45d403485a6c26fb3c0c13 Mon Sep 17 00:00:00 2001 From: maypok86 Date: Sun, 8 Sep 2024 11:27:20 +0300 Subject: [PATCH] [Chore] Refactor deletion --- builder.go | 26 +++++------ cache.go | 119 ++++++++++++++++++++------------------------------ cache_test.go | 88 ++++++++++++++++++------------------- deletion.go | 59 +++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 128 deletions(-) create mode 100644 deletion.go diff --git a/builder.go b/builder.go index e603c38..3df3833 100644 --- a/builder.go +++ b/builder.go @@ -21,15 +21,15 @@ import ( // Builder is a one-shot builder for creating a cache instance. type Builder[K comparable, V any] struct { - maximumSize *int - maximumWeight *uint64 - initialCapacity *int - statsRecorder StatsRecorder - ttl *time.Duration - withVariableTTL bool - weigher func(key K, value V) uint32 - deletionListener func(key K, value V, cause DeletionCause) - logger Logger + maximumSize *int + maximumWeight *uint64 + initialCapacity *int + statsRecorder StatsRecorder + ttl *time.Duration + withVariableTTL bool + weigher func(key K, value V) uint32 + onDeletion func(e DeletionEvent[K, V]) + logger Logger } // NewBuilder creates a builder and sets the future cache capacity. @@ -99,11 +99,11 @@ func (b *Builder[K, V]) Weigher(weigher func(key K, value V) uint32) *Builder[K, return b } -// DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any -// DeletionCause cause. The cache will invoke this listener in the background goroutine +// OnDeletion specifies a handler that caches should notify each time an entry is deleted for any +// DeletionCause. The cache will invoke this handler in the background goroutine // after the entry's deletion operation has completed. -func (b *Builder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *Builder[K, V] { - b.deletionListener = deletionListener +func (b *Builder[K, V]) OnDeletion(onDeletion func(e DeletionEvent[K, V])) *Builder[K, V] { + b.onDeletion = onDeletion return b } diff --git a/cache.go b/cache.go index ff4817e..3dbfbc4 100644 --- a/cache.go +++ b/cache.go @@ -30,35 +30,6 @@ import ( "github.com/maypok86/otter/v2/internal/xruntime" ) -// DeletionCause the cause why a cached entry was deleted. -type DeletionCause uint8 - -const ( - // Explicit the entry was manually deleted by the user. - Explicit DeletionCause = iota - // Replaced the entry itself was not actually deleted, but its value was replaced by the user. - Replaced - // Size the entry was evicted due to size constraints. - Size - // Expired the entry's expiration timestamp has passed. - Expired -) - -func (dc DeletionCause) String() string { - switch dc { - case Explicit: - return "Explicit" - case Replaced: - return "Replaced" - case Size: - return "Size" - case Expired: - return "Expired" - default: - panic("unknown deletion cause") - } -} - const ( minWriteBufferSize uint32 = 4 pinnedWeight uint32 = 0 @@ -95,26 +66,26 @@ type expiryPolicy[K comparable, V any] interface { // Cache is a structure performs a best-effort bounding of a hash table using eviction algorithm // to determine which entries to evict when the capacity is exceeded. type Cache[K comparable, V any] struct { - nodeManager *node.Manager[K, V] - hashmap *hashtable.Map[K, V] - policy evictionPolicy[K, V] - expiryPolicy expiryPolicy[K, V] - stats statsRecorder - logger Logger - clock *clock.Clock - stripedBuffer []*lossy.Buffer[K, V] - writeBuffer *queue.Growable[task[K, V]] - evictionMutex sync.Mutex - closeOnce sync.Once - doneClear chan struct{} - doneClose chan struct{} - weigher func(key K, value V) uint32 - deletionListener func(key K, value V, cause DeletionCause) - mask uint32 - ttl time.Duration - withExpiration bool - withEviction bool - withProcess bool + nodeManager *node.Manager[K, V] + hashmap *hashtable.Map[K, V] + policy evictionPolicy[K, V] + expiryPolicy expiryPolicy[K, V] + stats statsRecorder + logger Logger + clock *clock.Clock + stripedBuffer []*lossy.Buffer[K, V] + writeBuffer *queue.Growable[task[K, V]] + evictionMutex sync.Mutex + closeOnce sync.Once + doneClear chan struct{} + doneClose chan struct{} + weigher func(key K, value V) uint32 + onDeletion func(e DeletionEvent[K, V]) + mask uint32 + ttl time.Duration + withExpiration bool + withEviction bool + withProcess bool } // newCache returns a new cache instance based on the settings from Config. @@ -152,9 +123,9 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] { doneClear: make(chan struct{}), doneClose: make(chan struct{}, 1), //nolint:gosec // there will never be an overflow - mask: uint32(maxStripedBufferSize - 1), - weigher: b.getWeigher(), - deletionListener: b.deletionListener, + mask: uint32(maxStripedBufferSize - 1), + weigher: b.getWeigher(), + onDeletion: b.onDeletion, } cache.withEviction = withEviction @@ -342,7 +313,7 @@ func (c *Cache[K, V]) set(key K, value V, expiration int64, onlyIfAbsent bool) ( func (c *Cache[K, V]) afterWrite(n, evicted node.Node[K, V]) { if !c.withProcess { if evicted != nil { - c.notifyDeletion(n.Key(), n.Value(), Replaced) + c.notifyDeletion(n.Key(), n.Value(), CauseReplacement) } return } @@ -380,7 +351,7 @@ func (c *Cache[K, V]) afterDelete(deleted node.Node[K, V]) { } if !c.withProcess { - c.notifyDeletion(deleted.Key(), deleted.Value(), Explicit) + c.notifyDeletion(deleted.Key(), deleted.Value(), CauseInvalidation) return } @@ -388,14 +359,15 @@ func (c *Cache[K, V]) afterDelete(deleted node.Node[K, V]) { c.writeBuffer.Push(newDeleteTask(deleted)) } -// DeleteByFunc deletes the association for this key from the cache when the given function returns true. -func (c *Cache[K, V]) DeleteByFunc(f func(key K, value V) bool) { +// InvalidateByFunc deletes the association for this key from the cache when the given function returns true. +func (c *Cache[K, V]) InvalidateByFunc(fn func(key K, value V) bool) { + offset := c.clock.Offset() c.hashmap.Range(func(n node.Node[K, V]) bool { - if !n.IsAlive() || n.HasExpired(c.clock.Offset()) { + if !n.IsAlive() || n.HasExpired(offset) { return true } - if f(n.Key(), n.Value()) { + if fn(n.Key(), n.Value()) { c.deleteNode(n) } @@ -404,11 +376,15 @@ func (c *Cache[K, V]) DeleteByFunc(f func(key K, value V) bool) { } func (c *Cache[K, V]) notifyDeletion(key K, value V, cause DeletionCause) { - if c.deletionListener == nil { + if c.onDeletion == nil { return } - c.deletionListener(key, value, cause) + c.onDeletion(DeletionEvent[K, V]{ + Key: key, + Value: value, + Cause: cause, + }) } func (c *Cache[K, V]) deleteExpiredNode(n node.Node[K, V]) { @@ -416,7 +392,7 @@ func (c *Cache[K, V]) deleteExpiredNode(n node.Node[K, V]) { deleted := c.hashmap.DeleteNode(n) if deleted != nil { n.Die() - c.notifyDeletion(n.Key(), n.Value(), Expired) + c.notifyDeletion(n.Key(), n.Value(), CauseExpiration) c.stats.RecordEviction(n.Weight()) } } @@ -441,7 +417,7 @@ func (c *Cache[K, V]) evictNode(n node.Node[K, V]) { deleted := c.hashmap.DeleteNode(n) if deleted != nil { n.Die() - c.notifyDeletion(n.Key(), n.Value(), Size) + c.notifyDeletion(n.Key(), n.Value(), CauseOverflow) c.stats.RecordEviction(n.Weight()) } } @@ -491,12 +467,12 @@ func (c *Cache[K, V]) onWrite(t task[K, V]) { case t.isAdd(): c.addToPolicies(n) case t.isUpdate(): - c.deleteFromPolicies(t.oldNode(), Replaced) + c.deleteFromPolicies(t.oldNode(), CauseReplacement) c.addToPolicies(n) case t.isDelete(): - c.deleteFromPolicies(n, Explicit) + c.deleteFromPolicies(n, CauseInvalidation) case t.isExpired(): - c.deleteFromPolicies(n, Expired) + c.deleteFromPolicies(n, CauseExpiration) default: panic("invalid task type") } @@ -519,20 +495,21 @@ func (c *Cache[K, V]) process() { // Range iterates over all items in the cache. // // Iteration stops early when the given function returns false. -func (c *Cache[K, V]) Range(f func(key K, value V) bool) { +func (c *Cache[K, V]) Range(fn func(key K, value V) bool) { + offset := c.clock.Offset() c.hashmap.Range(func(n node.Node[K, V]) bool { - if !n.IsAlive() || n.HasExpired(c.clock.Offset()) { + if !n.IsAlive() || n.HasExpired(offset) { return true } - return f(n.Key(), n.Value()) + return fn(n.Key(), n.Value()) }) } -// Clear clears the hash table, all policies, buffers, etc. +// InvalidateAll discards all entries in the cache. // // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. -func (c *Cache[K, V]) Clear() { +func (c *Cache[K, V]) InvalidateAll() { c.clear(newClearTask[K, V]()) } @@ -553,7 +530,7 @@ func (c *Cache[K, V]) clear(t task[K, V]) { <-c.doneClear } -// Close clears the hash table, all policies, buffers, etc and stop all goroutines. +// Close discards all entries in the cache and stop all goroutines. // // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. func (c *Cache[K, V]) Close() { diff --git a/cache_test.go b/cache_test.go index c9c0351..a045c43 100644 --- a/cache_test.go +++ b/cache_test.go @@ -45,9 +45,9 @@ func TestCache_Unbounded(t *testing.T) { m := make(map[DeletionCause]int) mutex := sync.Mutex{} c, err := NewBuilder[int, int](). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). RecordStats(statsCounter). @@ -81,11 +81,11 @@ func TestCache_Unbounded(t *testing.T) { mutex.Lock() defer mutex.Unlock() - if len(m) != 2 || m[Explicit] != size-replaced { - t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size-replaced, m[Explicit]) + if len(m) != 2 || m[CauseInvalidation] != size-replaced { + t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size-replaced, m[CauseInvalidation]) } - if m[Replaced] != replaced { - t.Fatalf("cache was supposed to replace %d, but replaced %d entries", replaced, m[Replaced]) + if m[CauseReplacement] != replaced { + t.Fatalf("cache was supposed to replace %d, but replaced %d entries", replaced, m[CauseReplacement]) } if hitRatio := statsCounter.Snapshot().HitRatio(); hitRatio != 0.5 { t.Fatalf("not valid hit ratio. expected %.2f, but got %.2f", 0.5, hitRatio) @@ -106,9 +106,9 @@ func TestCache_PinnedWeight(t *testing.T) { return 1 }). WithTTL(3 * time.Second). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -147,11 +147,11 @@ func TestCache_PinnedWeight(t *testing.T) { mutex.Lock() defer mutex.Unlock() - if len(m) != 2 || m[Size] != size-1 { - t.Fatalf("cache was supposed to evict %d, but evicted %d entries", size-1, m[Size]) + if len(m) != 2 || m[CauseOverflow] != size-1 { + t.Fatalf("cache was supposed to evict %d, but evicted %d entries", size-1, m[CauseOverflow]) } - if m[Expired] != size+1 { - t.Fatalf("cache was supposed to expire %d, but expired %d entries", size+1, m[Expired]) + if m[CauseExpiration] != size+1 { + t.Fatalf("cache was supposed to expire %d, but expired %d entries", size+1, m[CauseExpiration]) } } @@ -257,7 +257,7 @@ func TestCache_Close(t *testing.T) { } } -func TestCache_Clear(t *testing.T) { +func TestCache_InvalidateAll(t *testing.T) { size := 10 c, err := NewBuilder[int, int](). MaximumSize(size). @@ -274,7 +274,7 @@ func TestCache_Clear(t *testing.T) { t.Fatalf("c.Size() = %d, want = %d", cacheSize, size) } - c.Clear() + c.InvalidateAll() time.Sleep(10 * time.Millisecond) @@ -292,9 +292,9 @@ func TestCache_Set(t *testing.T) { MaximumSize(size). WithTTL(time.Minute). RecordStats(statsCounter). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -345,8 +345,8 @@ func TestCache_Set(t *testing.T) { mutex.Lock() defer mutex.Unlock() - if len(m) != 1 || m[Replaced] != size { - t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[Replaced]) + if len(m) != 1 || m[CauseReplacement] != size { + t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[CauseReplacement]) } } @@ -380,7 +380,7 @@ func TestCache_SetIfAbsent(t *testing.T) { } } - c.Clear() + c.InvalidateAll() cc, err := NewBuilder[int, int](). MaximumSize(size). @@ -424,9 +424,9 @@ func TestCache_SetWithTTL(t *testing.T) { MaximumSize(size). InitialCapacity(size). WithTTL(time.Second). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -452,7 +452,7 @@ func TestCache_SetWithTTL(t *testing.T) { } mutex.Lock() - if e := m[Expired]; len(m) != 1 || e != size { + if e := m[CauseExpiration]; len(m) != 1 || e != size { mutex.Unlock() t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, e) } @@ -464,9 +464,9 @@ func TestCache_SetWithTTL(t *testing.T) { MaximumSize(size). WithVariableTTL(). RecordStats(statsCounter). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -496,8 +496,8 @@ func TestCache_SetWithTTL(t *testing.T) { } mutex.Lock() defer mutex.Unlock() - if len(m) != 1 || m[Expired] != size { - t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[Expired]) + if len(m) != 1 || m[CauseExpiration] != size { + t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[CauseExpiration]) } } @@ -509,9 +509,9 @@ func TestCache_Delete(t *testing.T) { MaximumSize(size). InitialCapacity(size). WithTTL(time.Hour). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -543,8 +543,8 @@ func TestCache_Delete(t *testing.T) { mutex.Lock() defer mutex.Unlock() - if len(m) != 1 || m[Explicit] != size { - t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size, m[Explicit]) + if len(m) != 1 || m[CauseInvalidation] != size { + t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size, m[CauseInvalidation]) } } @@ -556,9 +556,9 @@ func TestCache_DeleteByFunc(t *testing.T) { MaximumSize(size). InitialCapacity(size). WithTTL(time.Hour). - DeletionListener(func(key int, value int, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[int, int]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -570,7 +570,7 @@ func TestCache_DeleteByFunc(t *testing.T) { c.Set(i, i) } - c.DeleteByFunc(func(key int, value int) bool { + c.InvalidateByFunc(func(key int, value int) bool { return key%2 == 1 }) @@ -586,8 +586,8 @@ func TestCache_DeleteByFunc(t *testing.T) { expected := size / 2 mutex.Lock() defer mutex.Unlock() - if len(m) != 1 || m[Explicit] != expected { - t.Fatalf("cache was supposed to delete %d, but deleted %d entries", expected, m[Explicit]) + if len(m) != 1 || m[CauseInvalidation] != expected { + t.Fatalf("cache was supposed to delete %d, but deleted %d entries", expected, m[CauseInvalidation]) } } @@ -654,9 +654,9 @@ func TestCache_Ratio(t *testing.T) { c, err := NewBuilder[uint64, uint64](). MaximumSize(capacity). RecordStats(statsCounter). - DeletionListener(func(key uint64, value uint64, cause DeletionCause) { + OnDeletion(func(e DeletionEvent[uint64, uint64]) { mutex.Lock() - m[cause]++ + m[e.Cause]++ mutex.Unlock() }). Build() @@ -681,9 +681,9 @@ func TestCache_Ratio(t *testing.T) { mutex.Lock() defer mutex.Unlock() - t.Logf("evicted: %d", m[Size]) - if len(m) != 1 || m[Size] <= 0 || m[Size] > 5000 { - t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[Size]) + t.Logf("evicted: %d", m[CauseOverflow]) + if len(m) != 1 || m[CauseOverflow] <= 0 || m[CauseOverflow] > 5000 { + t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[CauseOverflow]) } } @@ -757,10 +757,10 @@ func (h *optimalHeap) Pop() any { func Test_GetExpired(t *testing.T) { c, err := NewBuilder[string, string](). RecordStats(stats.NewCounter()). - DeletionListener(func(key string, value string, cause DeletionCause) { - fmt.Println(cause) - if cause != Expired { - t.Fatalf("err not expired: %v", cause) + OnDeletion(func(e DeletionEvent[string, string]) { + fmt.Println(e.Cause) + if e.Cause != CauseExpiration { + t.Fatalf("err not expired: %v", e.Cause) } }). WithVariableTTL(). diff --git a/deletion.go b/deletion.go new file mode 100644 index 0000000..d4b5ba9 --- /dev/null +++ b/deletion.go @@ -0,0 +1,59 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otter + +// DeletionCause the cause why a cached entry was deleted. +type DeletionCause int + +const ( + // CauseInvalidation means that the entry was manually deleted by the user. + CauseInvalidation DeletionCause = iota + 1 + // CauseReplacement means that the entry itself was not actually deleted, but its value was replaced by the user. + CauseReplacement + // CauseOverflow means that the entry was evicted due to size constraints. + CauseOverflow + // CauseExpiration means that the entry's expiration timestamp has passed. + CauseExpiration +) + +var deletionCauseStrings = []string{ + "Invalidation", + "Replacement", + "Overflow", + "Expiration", +} + +func (dc DeletionCause) String() string { + if dc >= 1 && int(dc) <= len(deletionCauseStrings) { + return deletionCauseStrings[dc-1] + } + return "" +} + +// IsEviction returns true if there was an automatic deletion due to eviction +// (the cause is neither DeletionCauseInvalidation nor DeletionCauseReplacement). +func (dc DeletionCause) IsEviction() bool { + return !(dc == CauseInvalidation || dc == CauseReplacement) +} + +// DeletionEvent is an event of the deletion of a single entry. +type DeletionEvent[K comparable, V any] struct { + // Key is the key corresponding to the deleted entry. + Key K + // Value is the value corresponding to the deleted entry. + Value V + // Cause is the cause for which entry was deleted. + Cause DeletionCause +}