Skip to content
Merged
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
479 changes: 350 additions & 129 deletions pkg/eviction/arc.go

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions pkg/eviction/arc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package eviction

import "testing"

func TestARC_BasicSetGetAndEvict(t *testing.T) {
arc, err := NewARCAlgorithm(2)
if err != nil {
t.Fatalf("NewARCAlgorithm error: %v", err)
}

arc.Set("a", 1)
arc.Set("b", 2)
if v, ok := arc.Get("a"); !ok || v.(int) != 1 {
t.Fatalf("expected a=1, got %v ok=%v", v, ok)
}

// Insert c, causing an eviction
arc.Set("c", 3)
// One of a/b should be evicted; the other should remain.
if _, ok := arc.Get("a"); !ok {
if _, ok2 := arc.Get("b"); !ok2 {
t.Fatalf("expected one of 'a' or 'b' to remain resident")
}
}
}

func TestARC_ZeroCapacity_NoOp(t *testing.T) {
arc, err := NewARCAlgorithm(0)
if err != nil {
t.Fatalf("NewARCAlgorithm error: %v", err)
}
arc.Set("a", 1)
if _, ok := arc.Get("a"); ok {
t.Fatalf("expected miss on zero-capacity")
}
if key, ok := arc.Evict(); ok || key != "" {
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
}
}

func TestARC_Delete_RemovesResidentAndGhost(t *testing.T) {
arc, err := NewARCAlgorithm(2)
if err != nil {
t.Fatalf("NewARCAlgorithm error: %v", err)
}
arc.Set("a", 1)
arc.Set("b", 2)
arc.Delete("a")
if _, ok := arc.Get("a"); ok {
t.Fatalf("expected 'a' deleted")
}
// create a ghost by forcing eviction
arc.Set("c", 3)
arc.Delete("b") // whether resident or ghost, Delete should handle it
}

func TestARC_B1GhostHitPromotesToT2(t *testing.T) {
arc, err := NewARCAlgorithm(2)
if err != nil {
t.Fatalf("NewARCAlgorithm error: %v", err)
}
arc.Set("a", 1)
arc.Set("b", 2)
// cause eviction of one resident into ghosts by adding 'c'
arc.Set("c", 3)
// Now reinsert one of the early keys to hit a ghost (B1/B2) path
arc.Set("a", 10)
if v, ok := arc.Get("a"); !ok || v.(int) != 10 {
t.Fatalf("expected updated a=10 resident after B1/B2 hit, got %v ok=%v", v, ok)
}
}
4 changes: 3 additions & 1 deletion pkg/eviction/cawolfu.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ func (c *CAWOLFU) Evict() (string, bool) {
if err == nil {
c.length--

// Preserve key before resetting the node for pool reuse
evictedKey := node.key
resetCAWOLFUNode(node)
c.nodePool.Put(node)

return node.key, true
return evictedKey, true
}
// If map/list out of sync, forcibly clean up
resetCAWOLFUNode(node)
Expand Down
84 changes: 84 additions & 0 deletions pkg/eviction/cawolfu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package eviction

import "testing"

func TestCAWOLFU_EvictsLeastFrequentTail(t *testing.T) {
c, err := NewCAWOLFU(2)
if err != nil {
t.Fatalf("NewCAWOLFU error: %v", err)
}

c.Set("a", 1)
c.Set("b", 2)
// bump 'a' so 'b' is less frequent
if _, ok := c.Get("a"); !ok {
t.Fatalf("expected to get 'a'")
}

// Insert 'c' -> evict tail ('b')
c.Set("c", 3)
if _, ok := c.Get("b"); ok {
t.Fatalf("expected 'b' to be evicted")
}
if _, ok := c.Get("a"); !ok {
t.Fatalf("expected 'a' to remain in cache")
}
if v, ok := c.Get("c"); !ok || v.(int) != 3 {
t.Fatalf("expected 'c'=3 in cache, got %v, ok=%v", v, ok)
}
}

func TestCAWOLFU_EvictMethodOrder(t *testing.T) {
c, err := NewCAWOLFU(2)
if err != nil {
t.Fatalf("NewCAWOLFU error: %v", err)
}

c.Set("a", 1)
c.Set("b", 2)
// Without additional access, tail is 'a' (inserted first with same count)
key, ok := c.Evict()
if !ok || key != "a" {
t.Fatalf("expected to evict 'a' first, got %q ok=%v", key, ok)
}
key, ok = c.Evict()
if !ok || key != "b" {
t.Fatalf("expected to evict 'b' second, got %q ok=%v", key, ok)
}
}

func TestCAWOLFU_ZeroCapacity_NoOp(t *testing.T) {
c, err := NewCAWOLFU(0)
if err != nil {
t.Fatalf("NewCAWOLFU error: %v", err)
}

c.Set("a", 1)
if _, ok := c.Get("a"); ok {
t.Fatalf("expected Get to miss on zero-capacity cache")
}

if key, ok := c.Evict(); ok || key != "" {
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
}
}

func TestCAWOLFU_Delete_RemovesItem(t *testing.T) {
c, err := NewCAWOLFU(2)
if err != nil {
t.Fatalf("NewCAWOLFU error: %v", err)
}

c.Set("a", 1)
c.Set("b", 2)
c.Delete("a")

if _, ok := c.Get("a"); ok {
t.Fatalf("expected 'a' to be deleted")
}

key, ok := c.Evict()
if !ok || key != "b" {
t.Fatalf("expected to evict 'b' as remaining item, got %q ok=%v", key, ok)
}
}
6 changes: 4 additions & 2 deletions pkg/eviction/clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ func (c *ClockAlgorithm) Evict() (string, bool) {
if item.AccessCount > 0 {
item.AccessCount--
} else {
delete(c.keys, item.Key)
// Preserve key before zeroing the item back to the pool
evictedKey := item.Key
delete(c.keys, evictedKey)
c.itemPoolManager.Put(item)
c.items[c.hand] = nil

return item.Key, true
return evictedKey, true
}

c.hand = (c.hand + 1) % c.capacity
Expand Down
88 changes: 88 additions & 0 deletions pkg/eviction/clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package eviction

import "testing"

func TestClock_EvictsWhenHandFindsColdPage(t *testing.T) {
clk, err := NewClockAlgorithm(2)
if err != nil {
t.Fatalf("NewClockAlgorithm error: %v", err)
}

clk.Set("a", 1)
clk.Set("b", 2)

// Touch 'a' once to increment its AccessCount; leave 'b' cold
if _, ok := clk.Get("a"); !ok {
t.Fatalf("expected to get 'a'")
}

// Eviction may require multiple passes due to access count decrements.
// Loop until 'b' is evicted (within a few attempts).
var (
key string
ok bool
)
for i := 0; i < 3; i++ {
key, ok = clk.Evict()
if ok && key == "b" {
break
}
}
if !ok || key != "b" {
t.Fatalf("expected to evict 'b' first within retries, got %q ok=%v", key, ok)
}

// Now evict the remaining 'a', possibly requiring one more pass.
key, ok = clk.Evict()
if !ok {
key, ok = clk.Evict()
}
if !ok || key != "a" {
t.Fatalf("expected to evict 'a' second, got %q ok=%v", key, ok)
}
}

func TestClock_ZeroCapacity_NoOp(t *testing.T) {
clk, err := NewClockAlgorithm(0)
if err != nil {
t.Fatalf("NewClockAlgorithm error: %v", err)
}

clk.Set("a", 1)
if _, ok := clk.Get("a"); ok {
t.Fatalf("expected Get to miss on zero-capacity cache")
}

if key, ok := clk.Evict(); ok || key != "" {
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
}
}

func TestClock_Delete_RemovesItem(t *testing.T) {
clk, err := NewClockAlgorithm(2)
if err != nil {
t.Fatalf("NewClockAlgorithm error: %v", err)
}

clk.Set("a", 1)
clk.Set("b", 2)
clk.Delete("a")

if _, ok := clk.Get("a"); ok {
t.Fatalf("expected 'a' to be deleted")
}

// Evict should not return deleted key
// Loop a couple of times due to potential decrements
for i := 0; i < 3; i++ {
if key, ok := clk.Evict(); ok {
if key == "a" {
t.Fatalf("did not expect to evict deleted key 'a'")
}
if key == "b" {
return
}
}
}
t.Fatalf("expected to eventually evict 'b'")
}
98 changes: 98 additions & 0 deletions pkg/eviction/lfu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package eviction

import "testing"

// Test that when two items have equal frequency, the older (least-recent) is evicted first.
func TestLFU_EvictsOldestOnTie_InsertOrder(t *testing.T) {
lfu, err := NewLFUAlgorithm(2)
if err != nil {
t.Fatalf("NewLFUAlgorithm error: %v", err)
}

lfu.Set("a", 1)
lfu.Set("b", 2)

// Both have count=1, but "a" is older (inserted first), so it should be evicted first.
key, ok := lfu.Evict()
if !ok {
t.Fatalf("expected an eviction, got none")
}
if key != "a" {
t.Fatalf("expected 'a' to be evicted first on tie, got %q", key)
}

// Next eviction should evict the remaining one
key, ok = lfu.Evict()
if !ok {
t.Fatalf("expected a second eviction, got none")
}
if key != "b" {
t.Fatalf("expected 'b' to be evicted second, got %q", key)
}
}

// Test that recency is used to break ties after accesses with equalized frequency.
func TestLFU_EvictsOldestOnTie_AccessOrder(t *testing.T) {
lfu, err := NewLFUAlgorithm(2)
if err != nil {
t.Fatalf("NewLFUAlgorithm error: %v", err)
}

lfu.Set("a", 1)
lfu.Set("b", 2)

// Make both have the same frequency (2) but different recency orders.
// Access sequence: bump "b" first, then "a" -> both count=2, and "a" is more recent.
if _, ok := lfu.Get("b"); !ok {
t.Fatalf("expected to get 'b'")
}
if _, ok := lfu.Get("a"); !ok {
t.Fatalf("expected to get 'a'")
}

// On tie (count=2), the older (least-recent) is "b" now, so it should be evicted first.
key, ok := lfu.Evict()
if !ok {
t.Fatalf("expected an eviction, got none")
}
if key != "b" {
t.Fatalf("expected 'b' to be evicted first on tie after accesses, got %q", key)
}
}

func TestLFU_ZeroCapacity_NoOp(t *testing.T) {
lfu, err := NewLFUAlgorithm(0)
if err != nil {
t.Fatalf("NewLFUAlgorithm error: %v", err)
}

lfu.Set("a", 1)
if _, ok := lfu.Get("a"); ok {
t.Fatalf("expected Get to miss on zero-capacity cache")
}

if key, ok := lfu.Evict(); ok || key != "" {
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
}
}

func TestLFU_Delete_RemovesItem(t *testing.T) {
lfu, err := NewLFUAlgorithm(2)
if err != nil {
t.Fatalf("NewLFUAlgorithm error: %v", err)
}

lfu.Set("a", 1)
lfu.Set("b", 2)
lfu.Delete("a")

if _, ok := lfu.Get("a"); ok {
t.Fatalf("expected 'a' to be deleted")
}

// Evict should not return deleted key
key, ok := lfu.Evict()
if !ok || key != "b" {
t.Fatalf("expected to evict 'b' as remaining item, got %q ok=%v", key, ok)
}
}
Loading