Skip to content

Commit 1fe7a91

Browse files
authored
Merge pull request #44 from hyp3rd/feat/eviction-improvements
Feat/eviction improvements
2 parents 1be5936 + 0262ac1 commit 1fe7a91

8 files changed

Lines changed: 785 additions & 132 deletions

File tree

pkg/eviction/arc.go

Lines changed: 350 additions & 129 deletions
Large diffs are not rendered by default.

pkg/eviction/arc_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package eviction
2+
3+
import "testing"
4+
5+
func TestARC_BasicSetGetAndEvict(t *testing.T) {
6+
arc, err := NewARCAlgorithm(2)
7+
if err != nil {
8+
t.Fatalf("NewARCAlgorithm error: %v", err)
9+
}
10+
11+
arc.Set("a", 1)
12+
arc.Set("b", 2)
13+
if v, ok := arc.Get("a"); !ok || v.(int) != 1 {
14+
t.Fatalf("expected a=1, got %v ok=%v", v, ok)
15+
}
16+
17+
// Insert c, causing an eviction
18+
arc.Set("c", 3)
19+
// One of a/b should be evicted; the other should remain.
20+
if _, ok := arc.Get("a"); !ok {
21+
if _, ok2 := arc.Get("b"); !ok2 {
22+
t.Fatalf("expected one of 'a' or 'b' to remain resident")
23+
}
24+
}
25+
}
26+
27+
func TestARC_ZeroCapacity_NoOp(t *testing.T) {
28+
arc, err := NewARCAlgorithm(0)
29+
if err != nil {
30+
t.Fatalf("NewARCAlgorithm error: %v", err)
31+
}
32+
arc.Set("a", 1)
33+
if _, ok := arc.Get("a"); ok {
34+
t.Fatalf("expected miss on zero-capacity")
35+
}
36+
if key, ok := arc.Evict(); ok || key != "" {
37+
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
38+
}
39+
}
40+
41+
func TestARC_Delete_RemovesResidentAndGhost(t *testing.T) {
42+
arc, err := NewARCAlgorithm(2)
43+
if err != nil {
44+
t.Fatalf("NewARCAlgorithm error: %v", err)
45+
}
46+
arc.Set("a", 1)
47+
arc.Set("b", 2)
48+
arc.Delete("a")
49+
if _, ok := arc.Get("a"); ok {
50+
t.Fatalf("expected 'a' deleted")
51+
}
52+
// create a ghost by forcing eviction
53+
arc.Set("c", 3)
54+
arc.Delete("b") // whether resident or ghost, Delete should handle it
55+
}
56+
57+
func TestARC_B1GhostHitPromotesToT2(t *testing.T) {
58+
arc, err := NewARCAlgorithm(2)
59+
if err != nil {
60+
t.Fatalf("NewARCAlgorithm error: %v", err)
61+
}
62+
arc.Set("a", 1)
63+
arc.Set("b", 2)
64+
// cause eviction of one resident into ghosts by adding 'c'
65+
arc.Set("c", 3)
66+
// Now reinsert one of the early keys to hit a ghost (B1/B2) path
67+
arc.Set("a", 10)
68+
if v, ok := arc.Get("a"); !ok || v.(int) != 10 {
69+
t.Fatalf("expected updated a=10 resident after B1/B2 hit, got %v ok=%v", v, ok)
70+
}
71+
}

pkg/eviction/cawolfu.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,12 @@ func (c *CAWOLFU) Evict() (string, bool) {
6767
if err == nil {
6868
c.length--
6969

70+
// Preserve key before resetting the node for pool reuse
71+
evictedKey := node.key
7072
resetCAWOLFUNode(node)
7173
c.nodePool.Put(node)
7274

73-
return node.key, true
75+
return evictedKey, true
7476
}
7577
// If map/list out of sync, forcibly clean up
7678
resetCAWOLFUNode(node)

pkg/eviction/cawolfu_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package eviction
2+
3+
import "testing"
4+
5+
func TestCAWOLFU_EvictsLeastFrequentTail(t *testing.T) {
6+
c, err := NewCAWOLFU(2)
7+
if err != nil {
8+
t.Fatalf("NewCAWOLFU error: %v", err)
9+
}
10+
11+
c.Set("a", 1)
12+
c.Set("b", 2)
13+
// bump 'a' so 'b' is less frequent
14+
if _, ok := c.Get("a"); !ok {
15+
t.Fatalf("expected to get 'a'")
16+
}
17+
18+
// Insert 'c' -> evict tail ('b')
19+
c.Set("c", 3)
20+
if _, ok := c.Get("b"); ok {
21+
t.Fatalf("expected 'b' to be evicted")
22+
}
23+
if _, ok := c.Get("a"); !ok {
24+
t.Fatalf("expected 'a' to remain in cache")
25+
}
26+
if v, ok := c.Get("c"); !ok || v.(int) != 3 {
27+
t.Fatalf("expected 'c'=3 in cache, got %v, ok=%v", v, ok)
28+
}
29+
}
30+
31+
func TestCAWOLFU_EvictMethodOrder(t *testing.T) {
32+
c, err := NewCAWOLFU(2)
33+
if err != nil {
34+
t.Fatalf("NewCAWOLFU error: %v", err)
35+
}
36+
37+
c.Set("a", 1)
38+
c.Set("b", 2)
39+
// Without additional access, tail is 'a' (inserted first with same count)
40+
key, ok := c.Evict()
41+
if !ok || key != "a" {
42+
t.Fatalf("expected to evict 'a' first, got %q ok=%v", key, ok)
43+
}
44+
key, ok = c.Evict()
45+
if !ok || key != "b" {
46+
t.Fatalf("expected to evict 'b' second, got %q ok=%v", key, ok)
47+
}
48+
}
49+
50+
func TestCAWOLFU_ZeroCapacity_NoOp(t *testing.T) {
51+
c, err := NewCAWOLFU(0)
52+
if err != nil {
53+
t.Fatalf("NewCAWOLFU error: %v", err)
54+
}
55+
56+
c.Set("a", 1)
57+
if _, ok := c.Get("a"); ok {
58+
t.Fatalf("expected Get to miss on zero-capacity cache")
59+
}
60+
61+
if key, ok := c.Evict(); ok || key != "" {
62+
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
63+
}
64+
}
65+
66+
func TestCAWOLFU_Delete_RemovesItem(t *testing.T) {
67+
c, err := NewCAWOLFU(2)
68+
if err != nil {
69+
t.Fatalf("NewCAWOLFU error: %v", err)
70+
}
71+
72+
c.Set("a", 1)
73+
c.Set("b", 2)
74+
c.Delete("a")
75+
76+
if _, ok := c.Get("a"); ok {
77+
t.Fatalf("expected 'a' to be deleted")
78+
}
79+
80+
key, ok := c.Evict()
81+
if !ok || key != "b" {
82+
t.Fatalf("expected to evict 'b' as remaining item, got %q ok=%v", key, ok)
83+
}
84+
}

pkg/eviction/clock.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@ func (c *ClockAlgorithm) Evict() (string, bool) {
5757
if item.AccessCount > 0 {
5858
item.AccessCount--
5959
} else {
60-
delete(c.keys, item.Key)
60+
// Preserve key before zeroing the item back to the pool
61+
evictedKey := item.Key
62+
delete(c.keys, evictedKey)
6163
c.itemPoolManager.Put(item)
6264
c.items[c.hand] = nil
6365

64-
return item.Key, true
66+
return evictedKey, true
6567
}
6668

6769
c.hand = (c.hand + 1) % c.capacity

pkg/eviction/clock_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package eviction
2+
3+
import "testing"
4+
5+
func TestClock_EvictsWhenHandFindsColdPage(t *testing.T) {
6+
clk, err := NewClockAlgorithm(2)
7+
if err != nil {
8+
t.Fatalf("NewClockAlgorithm error: %v", err)
9+
}
10+
11+
clk.Set("a", 1)
12+
clk.Set("b", 2)
13+
14+
// Touch 'a' once to increment its AccessCount; leave 'b' cold
15+
if _, ok := clk.Get("a"); !ok {
16+
t.Fatalf("expected to get 'a'")
17+
}
18+
19+
// Eviction may require multiple passes due to access count decrements.
20+
// Loop until 'b' is evicted (within a few attempts).
21+
var (
22+
key string
23+
ok bool
24+
)
25+
for i := 0; i < 3; i++ {
26+
key, ok = clk.Evict()
27+
if ok && key == "b" {
28+
break
29+
}
30+
}
31+
if !ok || key != "b" {
32+
t.Fatalf("expected to evict 'b' first within retries, got %q ok=%v", key, ok)
33+
}
34+
35+
// Now evict the remaining 'a', possibly requiring one more pass.
36+
key, ok = clk.Evict()
37+
if !ok {
38+
key, ok = clk.Evict()
39+
}
40+
if !ok || key != "a" {
41+
t.Fatalf("expected to evict 'a' second, got %q ok=%v", key, ok)
42+
}
43+
}
44+
45+
func TestClock_ZeroCapacity_NoOp(t *testing.T) {
46+
clk, err := NewClockAlgorithm(0)
47+
if err != nil {
48+
t.Fatalf("NewClockAlgorithm error: %v", err)
49+
}
50+
51+
clk.Set("a", 1)
52+
if _, ok := clk.Get("a"); ok {
53+
t.Fatalf("expected Get to miss on zero-capacity cache")
54+
}
55+
56+
if key, ok := clk.Evict(); ok || key != "" {
57+
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
58+
}
59+
}
60+
61+
func TestClock_Delete_RemovesItem(t *testing.T) {
62+
clk, err := NewClockAlgorithm(2)
63+
if err != nil {
64+
t.Fatalf("NewClockAlgorithm error: %v", err)
65+
}
66+
67+
clk.Set("a", 1)
68+
clk.Set("b", 2)
69+
clk.Delete("a")
70+
71+
if _, ok := clk.Get("a"); ok {
72+
t.Fatalf("expected 'a' to be deleted")
73+
}
74+
75+
// Evict should not return deleted key
76+
// Loop a couple of times due to potential decrements
77+
for i := 0; i < 3; i++ {
78+
if key, ok := clk.Evict(); ok {
79+
if key == "a" {
80+
t.Fatalf("did not expect to evict deleted key 'a'")
81+
}
82+
if key == "b" {
83+
return
84+
}
85+
}
86+
}
87+
t.Fatalf("expected to eventually evict 'b'")
88+
}

pkg/eviction/lfu_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package eviction
2+
3+
import "testing"
4+
5+
// Test that when two items have equal frequency, the older (least-recent) is evicted first.
6+
func TestLFU_EvictsOldestOnTie_InsertOrder(t *testing.T) {
7+
lfu, err := NewLFUAlgorithm(2)
8+
if err != nil {
9+
t.Fatalf("NewLFUAlgorithm error: %v", err)
10+
}
11+
12+
lfu.Set("a", 1)
13+
lfu.Set("b", 2)
14+
15+
// Both have count=1, but "a" is older (inserted first), so it should be evicted first.
16+
key, ok := lfu.Evict()
17+
if !ok {
18+
t.Fatalf("expected an eviction, got none")
19+
}
20+
if key != "a" {
21+
t.Fatalf("expected 'a' to be evicted first on tie, got %q", key)
22+
}
23+
24+
// Next eviction should evict the remaining one
25+
key, ok = lfu.Evict()
26+
if !ok {
27+
t.Fatalf("expected a second eviction, got none")
28+
}
29+
if key != "b" {
30+
t.Fatalf("expected 'b' to be evicted second, got %q", key)
31+
}
32+
}
33+
34+
// Test that recency is used to break ties after accesses with equalized frequency.
35+
func TestLFU_EvictsOldestOnTie_AccessOrder(t *testing.T) {
36+
lfu, err := NewLFUAlgorithm(2)
37+
if err != nil {
38+
t.Fatalf("NewLFUAlgorithm error: %v", err)
39+
}
40+
41+
lfu.Set("a", 1)
42+
lfu.Set("b", 2)
43+
44+
// Make both have the same frequency (2) but different recency orders.
45+
// Access sequence: bump "b" first, then "a" -> both count=2, and "a" is more recent.
46+
if _, ok := lfu.Get("b"); !ok {
47+
t.Fatalf("expected to get 'b'")
48+
}
49+
if _, ok := lfu.Get("a"); !ok {
50+
t.Fatalf("expected to get 'a'")
51+
}
52+
53+
// On tie (count=2), the older (least-recent) is "b" now, so it should be evicted first.
54+
key, ok := lfu.Evict()
55+
if !ok {
56+
t.Fatalf("expected an eviction, got none")
57+
}
58+
if key != "b" {
59+
t.Fatalf("expected 'b' to be evicted first on tie after accesses, got %q", key)
60+
}
61+
}
62+
63+
func TestLFU_ZeroCapacity_NoOp(t *testing.T) {
64+
lfu, err := NewLFUAlgorithm(0)
65+
if err != nil {
66+
t.Fatalf("NewLFUAlgorithm error: %v", err)
67+
}
68+
69+
lfu.Set("a", 1)
70+
if _, ok := lfu.Get("a"); ok {
71+
t.Fatalf("expected Get to miss on zero-capacity cache")
72+
}
73+
74+
if key, ok := lfu.Evict(); ok || key != "" {
75+
t.Fatalf("expected no eviction on zero-capacity, got %q ok=%v", key, ok)
76+
}
77+
}
78+
79+
func TestLFU_Delete_RemovesItem(t *testing.T) {
80+
lfu, err := NewLFUAlgorithm(2)
81+
if err != nil {
82+
t.Fatalf("NewLFUAlgorithm error: %v", err)
83+
}
84+
85+
lfu.Set("a", 1)
86+
lfu.Set("b", 2)
87+
lfu.Delete("a")
88+
89+
if _, ok := lfu.Get("a"); ok {
90+
t.Fatalf("expected 'a' to be deleted")
91+
}
92+
93+
// Evict should not return deleted key
94+
key, ok := lfu.Evict()
95+
if !ok || key != "b" {
96+
t.Fatalf("expected to evict 'b' as remaining item, got %q ok=%v", key, ok)
97+
}
98+
}

0 commit comments

Comments
 (0)