diff --git a/builder.go b/builder.go index d21b738..4a6c150 100644 --- a/builder.go +++ b/builder.go @@ -91,6 +91,9 @@ func (b *Builder[K, V]) InitialCapacity(initialCapacity int) *Builder[K, V] { // of this method requires a corresponding call to MaximumWeight prior to calling Build. // Weights are measured and recorded when entries are inserted into or updated in // the cache, and are thus effectively static during the lifetime of a cache entry. +// +// When the weight of an entry is zero it will not be considered for size-based eviction (though +// it still may be evicted by other means). func (b *Builder[K, V]) Weigher(weigher func(key K, value V) uint32) *Builder[K, V] { b.weigher = weigher return b diff --git a/cache.go b/cache.go index 82a8377..4228f3f 100644 --- a/cache.go +++ b/cache.go @@ -61,6 +61,7 @@ func (dc DeletionCause) String() string { const ( minWriteBufferSize uint32 = 4 + pinnedWeight uint32 = 0 ) var ( @@ -436,6 +437,23 @@ func (c *Cache[K, V]) evictNode(n node.Node[K, V]) { } } +func (c *Cache[K, V]) addToPolicies(n node.Node[K, V]) { + if !n.IsAlive() { + return + } + + c.expiryPolicy.Add(n) + if n.Weight() != pinnedWeight { + c.policy.Add(n, c.clock.Offset()) + } +} + +func (c *Cache[K, V]) deleteFromPolicies(n node.Node[K, V], cause DeletionCause) { + c.expiryPolicy.Delete(n) + c.policy.Delete(n) + c.notifyDeletion(n.Key(), n.Value(), cause) +} + func (c *Cache[K, V]) onWrite(t task[K, V]) { if t.isClear() || t.isClose() { c.writeBuffer.Clear() @@ -453,27 +471,16 @@ func (c *Cache[K, V]) onWrite(t task[K, V]) { n := t.node() switch { case t.isAdd(): - if n.IsAlive() { - c.expiryPolicy.Add(n) - c.policy.Add(n, c.clock.Offset()) - } + c.addToPolicies(n) case t.isUpdate(): - oldNode := t.oldNode() - c.expiryPolicy.Delete(oldNode) - c.policy.Delete(oldNode) - if n.IsAlive() { - c.expiryPolicy.Add(n) - c.policy.Add(n, c.clock.Offset()) - } - c.notifyDeletion(oldNode.Key(), oldNode.Value(), Replaced) + c.deleteFromPolicies(t.oldNode(), Replaced) + c.addToPolicies(n) case t.isDelete(): - c.expiryPolicy.Delete(n) - c.policy.Delete(n) - c.notifyDeletion(n.Key(), n.Value(), Explicit) + c.deleteFromPolicies(n, Explicit) case t.isExpired(): - c.expiryPolicy.Delete(n) - c.policy.Delete(n) - c.notifyDeletion(n.Key(), n.Value(), Expired) + c.deleteFromPolicies(n, Expired) + default: + panic("invalid task type") } } diff --git a/cache_test.go b/cache_test.go index f3b00e8..4d64544 100644 --- a/cache_test.go +++ b/cache_test.go @@ -87,6 +87,72 @@ func TestCache_Unbounded(t *testing.T) { if m[Replaced] != replaced { t.Fatalf("cache was supposed to replace %d, but replaced %d entries", replaced, m[Replaced]) } + if hitRatio := statsCounter.Snapshot().HitRatio(); hitRatio != 0.5 { + t.Fatalf("not valid hit ratio. expected %.2f, but got %.2f", 0.5, hitRatio) + } +} + +func TestCache_PinnedWeight(t *testing.T) { + size := 10 + pinned := 4 + m := make(map[DeletionCause]int) + mutex := sync.Mutex{} + c, err := NewBuilder[int, int](). + MaximumWeight(uint64(size)). + Weigher(func(key int, value int) uint32 { + if key == pinned { + return pinnedWeight + } + return 1 + }). + WithTTL(3 * time.Second). + DeletionListener(func(key int, value int, cause DeletionCause) { + mutex.Lock() + m[cause]++ + mutex.Unlock() + }). + Build() + if err != nil { + t.Fatalf("can not create cache: %v", err) + } + + for i := 0; i < size; i++ { + c.Set(i, i) + } + for i := 0; i < size; i++ { + if !c.Has(i) { + t.Fatalf("the key must exist: %d", i) + } + c.Has(i) + } + for i := size; i < 2*size; i++ { + c.Set(i, i) + } + time.Sleep(time.Second) + for i := size; i < 2*size; i++ { + if !c.Has(i) { + t.Fatalf("the key must exist: %d", i) + } + c.Has(i) + } + if !c.Has(pinned) { + t.Fatalf("the key must exist: %d", pinned) + } + + time.Sleep(3 * time.Second) + + if c.Has(pinned) { + t.Fatalf("the key must not exist: %d", pinned) + } + + 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 m[Expired] != size+1 { + t.Fatalf("cache was supposed to expire %d, but expired %d entries", size+1, m[Expired]) + } } func TestCache_SetWithWeight(t *testing.T) {