Skip to content

Commit 2b5c6bc

Browse files
asaadbalumAsaad Balumrootfs
authored
feat(cache): implement O(1) eviction policies and O(k) TTL cleanup (#781)
* feat(cache): implement O(1) eviction policies and O(k) TTL cleanup Addresses Issue #162: Implementation: - FIFO: Doubly-linked list queue for O(1) oldest entry selection - LRU: Doubly-linked list + hashmap for O(1) least-recently-used tracking - LFU: Frequency buckets with doubly-linked lists for O(1) least-frequently-used tracking - ExpirationHeap: Min-heap for O(k) TTL cleanup (k = expired entries) Performance (10k entries): - FIFO SelectVictim: ~4.3 ns/op - LRU SelectVictim: ~4.3 ns/op - LFU SelectVictim: ~6.2 ns/op Note: Issues 1 and 2 from #162 were already addressed in PR #347 and PR #504. Signed-off-by: Asaad Balum <abalum@abalum-thinkpadp16vgen1.raanaii.csb> * fix: remove unused idx variable in test to pass ineffassign lint Signed-off-by: Asaad Balum <abalum@abalum-thinkpadp16vgen1.raanaii.csb> --------- Signed-off-by: Asaad Balum <abalum@abalum-thinkpadp16vgen1.raanaii.csb> Co-authored-by: Asaad Balum <abalum@abalum-thinkpadp16vgen1.raanaii.csb> Co-authored-by: Huamin Chen <rootfs@users.noreply.github.com>
1 parent 90c7cda commit 2b5c6bc

File tree

3 files changed

+1064
-44
lines changed

3 files changed

+1064
-44
lines changed

src/semantic-router/pkg/cache/cache_test.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,224 @@ func TestLFUPolicyTiebreaker(t *testing.T) {
13511351
}
13521352
}
13531353

1354+
// TestFIFOPolicyOptimized tests the O(1) FIFO policy operations
1355+
func TestFIFOPolicyOptimized(t *testing.T) {
1356+
policy := NewFIFOPolicy()
1357+
entries := []CacheEntry{
1358+
{RequestID: "req-0"},
1359+
{RequestID: "req-1"},
1360+
{RequestID: "req-2"},
1361+
}
1362+
1363+
// Test OnInsert and SelectVictim
1364+
for i, e := range entries {
1365+
policy.OnInsert(i, e.RequestID)
1366+
}
1367+
1368+
victim := policy.SelectVictim(entries)
1369+
if victim != 0 {
1370+
t.Errorf("Expected victim 0 (oldest), got %d", victim)
1371+
}
1372+
1373+
// Test Evict
1374+
evicted := policy.Evict()
1375+
if evicted != 0 {
1376+
t.Errorf("Expected evicted index 0, got %d", evicted)
1377+
}
1378+
1379+
// Test UpdateIndex (simulating swap after eviction)
1380+
policy.UpdateIndex("req-2", 2, 0)
1381+
victim = policy.SelectVictim(entries)
1382+
if victim != 1 {
1383+
t.Errorf("Expected victim 1 after swap, got %d", victim)
1384+
}
1385+
1386+
// Test OnRemove
1387+
policy.OnRemove(1, "req-1")
1388+
victim = policy.SelectVictim(entries)
1389+
if victim != 0 {
1390+
t.Errorf("Expected victim 0 (req-2 moved), got %d", victim)
1391+
}
1392+
}
1393+
1394+
// TestLRUPolicyOptimized tests the O(1) LRU policy operations
1395+
func TestLRUPolicyOptimized(t *testing.T) {
1396+
policy := NewLRUPolicy()
1397+
entries := []CacheEntry{
1398+
{RequestID: "req-0"},
1399+
{RequestID: "req-1"},
1400+
{RequestID: "req-2"},
1401+
}
1402+
1403+
// Insert all entries
1404+
for i, e := range entries {
1405+
policy.OnInsert(i, e.RequestID)
1406+
}
1407+
1408+
// LRU order: req-2 (MRU) -> req-1 -> req-0 (LRU)
1409+
victim := policy.SelectVictim(entries)
1410+
if victim != 0 {
1411+
t.Errorf("Expected victim 0 (LRU), got %d", victim)
1412+
}
1413+
1414+
// Access req-0 to make it MRU
1415+
policy.OnAccess(0, "req-0")
1416+
victim = policy.SelectVictim(entries)
1417+
if victim != 1 {
1418+
t.Errorf("Expected victim 1 after accessing req-0, got %d", victim)
1419+
}
1420+
1421+
// Test Evict
1422+
evicted := policy.Evict()
1423+
if evicted != 1 {
1424+
t.Errorf("Expected evicted index 1, got %d", evicted)
1425+
}
1426+
1427+
// Test UpdateIndex
1428+
policy.UpdateIndex("req-2", 2, 1)
1429+
1430+
// Test OnRemove
1431+
policy.OnRemove(1, "req-2")
1432+
1433+
// Only req-0 should remain
1434+
victim = policy.SelectVictim(entries)
1435+
if victim != 0 {
1436+
t.Errorf("Expected victim 0, got %d", victim)
1437+
}
1438+
}
1439+
1440+
// TestLFUPolicyOptimized tests the O(1) LFU policy operations
1441+
func TestLFUPolicyOptimized(t *testing.T) {
1442+
policy := NewLFUPolicy()
1443+
entries := []CacheEntry{
1444+
{RequestID: "req-0"},
1445+
{RequestID: "req-1"},
1446+
{RequestID: "req-2"},
1447+
}
1448+
1449+
// Insert all entries (all start with freq=1)
1450+
for i, e := range entries {
1451+
policy.OnInsert(i, e.RequestID)
1452+
}
1453+
1454+
// Access req-2 multiple times to increase frequency
1455+
for i := 0; i < 5; i++ {
1456+
policy.OnAccess(2, "req-2")
1457+
}
1458+
1459+
// req-0 and req-1 have freq=1, req-2 has freq=6
1460+
victim := policy.SelectVictim(entries)
1461+
if victim != 0 && victim != 1 {
1462+
t.Errorf("Expected victim 0 or 1 (lowest freq), got %d", victim)
1463+
}
1464+
1465+
// Test Evict
1466+
evicted := policy.Evict()
1467+
if evicted != 0 && evicted != 1 {
1468+
t.Errorf("Expected evicted 0 or 1, got %d", evicted)
1469+
}
1470+
1471+
// Test UpdateIndex
1472+
policy.UpdateIndex("req-2", 2, 0)
1473+
1474+
// Test OnRemove
1475+
policy.OnRemove(1, "req-1")
1476+
}
1477+
1478+
// TestExpirationHeapOperations tests all ExpirationHeap operations
1479+
func TestExpirationHeapOperations(t *testing.T) {
1480+
now := time.Now()
1481+
heap := NewExpirationHeap()
1482+
1483+
// Test Add
1484+
heap.Add("req-0", 0, now.Add(1*time.Hour))
1485+
heap.Add("req-1", 1, now.Add(30*time.Minute))
1486+
heap.Add("req-2", 2, now.Add(2*time.Hour))
1487+
1488+
// Test Size
1489+
if heap.Size() != 3 {
1490+
t.Errorf("Expected size 3, got %d", heap.Size())
1491+
}
1492+
1493+
// Test PeekNext (should be req-1, earliest expiration)
1494+
reqID, idx, expiresAt, ok := heap.PeekNext()
1495+
if !ok || reqID != "req-1" || idx != 1 {
1496+
t.Errorf("Expected PeekNext to return req-1, got %s (idx=%d, ok=%v)", reqID, idx, ok)
1497+
}
1498+
_ = expiresAt
1499+
1500+
// Test UpdateExpiration (move req-1 to later)
1501+
heap.UpdateExpiration("req-1", now.Add(3*time.Hour))
1502+
1503+
// Now req-0 should be earliest
1504+
reqID, _, _, ok = heap.PeekNext()
1505+
if !ok || reqID != "req-0" {
1506+
t.Errorf("Expected PeekNext to return req-0 after update, got %s", reqID)
1507+
}
1508+
1509+
// Test UpdateIndex
1510+
heap.UpdateIndex("req-0", 5)
1511+
1512+
// Test Remove
1513+
heap.Remove("req-0")
1514+
if heap.Size() != 2 {
1515+
t.Errorf("Expected size 2 after remove, got %d", heap.Size())
1516+
}
1517+
1518+
// Test PopExpired
1519+
heap.Add("req-expired", 10, now.Add(-1*time.Hour)) // Already expired
1520+
expired := heap.PopExpired(now)
1521+
if len(expired) != 1 || expired[0] != "req-expired" {
1522+
t.Errorf("Expected 1 expired entry, got %v", expired)
1523+
}
1524+
}
1525+
1526+
// TestInMemoryCacheEviction tests cache eviction with O(1) policies
1527+
func TestInMemoryCacheEviction(t *testing.T) {
1528+
cache := NewInMemoryCache(InMemoryCacheOptions{
1529+
Enabled: true,
1530+
MaxEntries: 3,
1531+
TTLSeconds: 3600,
1532+
SimilarityThreshold: 0.9,
1533+
EvictionPolicy: LRUEvictionPolicyType,
1534+
})
1535+
1536+
// Add entries up to max
1537+
for i := 0; i < 3; i++ {
1538+
embedding := make([]float32, 384)
1539+
embedding[i] = 1.0
1540+
cache.mu.Lock()
1541+
cache.entries = append(cache.entries, CacheEntry{
1542+
RequestID: fmt.Sprintf("req-%d", i),
1543+
Query: fmt.Sprintf("query %d", i),
1544+
Embedding: embedding,
1545+
})
1546+
idx := len(cache.entries) - 1
1547+
cache.entryMap[fmt.Sprintf("req-%d", i)] = idx
1548+
cache.registerEntryWithEvictionPolicy(idx, fmt.Sprintf("req-%d", i))
1549+
cache.mu.Unlock()
1550+
}
1551+
1552+
// Verify we have 3 entries
1553+
stats := cache.GetStats()
1554+
if stats.TotalEntries != 3 {
1555+
t.Errorf("Expected 3 entries, got %d", stats.TotalEntries)
1556+
}
1557+
1558+
// Add one more to trigger eviction
1559+
cache.mu.Lock()
1560+
cache.evictOne()
1561+
cache.mu.Unlock()
1562+
1563+
// Should have 2 entries now
1564+
cache.mu.RLock()
1565+
count := len(cache.entries)
1566+
cache.mu.RUnlock()
1567+
if count != 2 {
1568+
t.Errorf("Expected 2 entries after eviction, got %d", count)
1569+
}
1570+
}
1571+
13541572
// TestHybridCacheDisabled tests that disabled hybrid cache returns immediately
13551573
func TestHybridCacheDisabled(t *testing.T) {
13561574
cache, err := NewHybridCache(HybridCacheOptions{

0 commit comments

Comments
 (0)