- prompts/: LFU cache exam + 5-pillar grading rubric - outputs/: 8 model .py outputs (local + cloud baseline) - data/benchmark_history.json: graded results (scores, metrics, bugs, patches) - generate_dashboard.py: builds dashboard.html + pages/*.html from JSON - Dockerfile + DEPLOY.md: Gitea→Coolify deploy (build-step, nginx static) - .gitignore: generated HTML excluded (built on deploy) Co-Authored-By: Claude <noreply@anthropic.com>
366 lines
13 KiB
Python
366 lines
13 KiB
Python
"""
|
|
In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions.
|
|
Pure Python 3.11+ implementation using only standard library modules.
|
|
"""
|
|
|
|
import asyncio
|
|
import time
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from asyncio import Lock
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal Data Structures (O(1) LFU Core)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class _Node:
|
|
"""Doubly linked list node tracking key, value, frequency, and TTL."""
|
|
__slots__ = ('key', 'value', 'freq', 'expires_at', 'prev', 'next')
|
|
def __init__(self, key: Any, value: Any, freq: int = 1, expires_at: float = 0.0):
|
|
self.key = key
|
|
self.value = value
|
|
self.freq = freq
|
|
self.expires_at = expires_at
|
|
self.prev = None
|
|
self.next = None
|
|
|
|
class _FreqList:
|
|
"""Doubly linked list maintaining nodes of a specific frequency tier."""
|
|
__slots__ = ('head', 'tail', 'size')
|
|
def __init__(self):
|
|
self.head = _Node(None, None) # Dummy head
|
|
self.tail = _Node(None, None) # Dummy tail
|
|
self.head.next = self.tail
|
|
self.tail.prev = self.head
|
|
self.size = 0
|
|
|
|
def add(self, node: _Node):
|
|
"""Add node to tail (most recently used in this frequency)."""
|
|
last = self.tail.prev
|
|
last.next = node
|
|
node.prev = last
|
|
node.next = self.tail
|
|
self.tail.prev = node
|
|
self.size += 1
|
|
|
|
def remove(self, node: _Node):
|
|
"""Remove node from the list in O(1)."""
|
|
node.prev.next = node.next
|
|
node.next.prev = node.prev
|
|
node.prev = None
|
|
node.next = None
|
|
self.size -= 1
|
|
|
|
def pop(self) -> _Node:
|
|
"""Remove and return node from head (least recently used)."""
|
|
node = self.head.next
|
|
self.remove(node)
|
|
return node
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main Cache Implementation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class LFUCache:
|
|
"""
|
|
O(1) In-Memory Concurrent LFU Cache with Async TTL Eviction.
|
|
Uses a hash map for key lookup and a hash map of frequency-buckets
|
|
with doubly linked lists for O(1) frequency updates and eviction.
|
|
"""
|
|
def __init__(self, capacity: int, evict_interval: float = 1.0):
|
|
self.capacity = capacity
|
|
self.evict_interval = evict_interval
|
|
self.key_to_node: Dict[Any, _Node] = {}
|
|
self.freq_to_list: Dict[int, _FreqList] = {}
|
|
self.min_freq: int = 1
|
|
self.lock: Lock = asyncio.Lock()
|
|
self._evict_task: Optional[asyncio.Task] = None
|
|
self._running = False
|
|
|
|
async def start_evictor(self):
|
|
"""Start the non-blocking background TTL eviction loop."""
|
|
self._running = True
|
|
self._evict_task = asyncio.create_task(self._eviction_loop())
|
|
|
|
async def stop_evictor(self):
|
|
"""Gracefully stop the background eviction task."""
|
|
self._running = False
|
|
if self._evict_task:
|
|
self._evict_task.cancel()
|
|
try: await self._evict_task
|
|
except asyncio.CancelledError: pass
|
|
|
|
async def get(self, key: Any) -> Optional[Any]:
|
|
"""Retrieve value in O(1). Performs lazy TTL eviction and frequency update."""
|
|
async with self.lock:
|
|
node = self.key_to_node.get(key)
|
|
if not node:
|
|
return None
|
|
|
|
# Lazy TTL Eviction
|
|
if node.expires_at > 0 and node.expires_at <= time.time():
|
|
self._remove_node(node)
|
|
return None
|
|
|
|
# O(1) Frequency Update
|
|
self._update_freq(node)
|
|
return node.value
|
|
|
|
async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0):
|
|
"""Insert/update value in O(1). Handles capacity eviction and TTL."""
|
|
async with self.lock:
|
|
node = self.key_to_node.get(key)
|
|
if node:
|
|
# Update existing node
|
|
node.value = value
|
|
if ttl_seconds > 0:
|
|
node.expires_at = time.time() + ttl_seconds
|
|
self._update_freq(node)
|
|
else:
|
|
# Evict if capacity reached
|
|
if len(self.key_to_node) >= self.capacity:
|
|
self._evict()
|
|
# Insert new node with freq=1
|
|
node = _Node(
|
|
key, value, freq=1,
|
|
expires_at=float('inf') if ttl_seconds <= 0 else time.time() + ttl_seconds
|
|
)
|
|
self.key_to_node[key] = node
|
|
self._add_to_freq(node)
|
|
self.min_freq = 1 # New keys always start at freq 1
|
|
|
|
async def begin_transaction(self) -> 'Transaction':
|
|
"""Create an isolated transaction session."""
|
|
return Transaction(self)
|
|
|
|
# --- Internal Cache Helpers (O(1)) ---
|
|
|
|
def _remove_node(self, node: _Node):
|
|
"""Remove node from global structures."""
|
|
del self.key_to_node[node.key]
|
|
self.freq_to_list[node.freq].remove(node)
|
|
if self.freq_to_list[node.freq].size == 0:
|
|
del self.freq_to_list[node.freq]
|
|
if self.min_freq == node.freq:
|
|
self.min_freq += 1
|
|
|
|
def _update_freq(self, node: _Node):
|
|
"""Move node to next frequency bucket in O(1)."""
|
|
old_freq = node.freq
|
|
self.freq_to_list[old_freq].remove(node)
|
|
if self.freq_to_list[old_freq].size == 0:
|
|
del self.freq_to_list[old_freq]
|
|
if self.min_freq == old_freq:
|
|
self.min_freq += 1
|
|
|
|
node.freq += 1
|
|
self._add_to_freq(node)
|
|
|
|
def _add_to_freq(self, node: _Node):
|
|
"""Add node to its frequency bucket."""
|
|
if node.freq not in self.freq_to_list:
|
|
self.freq_to_list[node.freq] = _FreqList()
|
|
self.freq_to_list[node.freq].add(node)
|
|
|
|
def _evict(self):
|
|
"""Evict least frequently used (then LRU) node in O(1)."""
|
|
if not self.freq_to_list:
|
|
return
|
|
evict_list = self.freq_to_list[self.min_freq]
|
|
node = evict_list.pop()
|
|
self._remove_node(node)
|
|
|
|
async def _eviction_loop(self):
|
|
"""Background async task that purges expired keys in small batches."""
|
|
while self._running:
|
|
await asyncio.sleep(self.evict_interval)
|
|
async with self.lock:
|
|
# Batch scan limited to avoid blocking reads
|
|
keys_to_check = list(self.key_to_node.keys())[:50]
|
|
for k in keys_to_check:
|
|
node = self.key_to_node.get(k)
|
|
if node and node.expires_at > 0 and node.expires_at <= time.time():
|
|
self._remove_node(node)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Transaction Isolation Layer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class Transaction:
|
|
"""
|
|
Atomic transaction session providing Read-Your-Own-Writes isolation.
|
|
Global readers remain blind to uncommitted changes until commit().
|
|
"""
|
|
def __init__(self, cache: LFUCache):
|
|
self.cache = cache
|
|
self.buffer: List[Dict] = []
|
|
self.committed = False
|
|
|
|
async def get(self, key: Any) -> Optional[Any]:
|
|
"""Read with local isolation. Does not update global frequencies."""
|
|
# Read-Your-Own-Writes: check buffer first
|
|
for op in reversed(self.buffer):
|
|
if op['key'] == key:
|
|
return None if op['op'] == 'delete' else op['value']
|
|
|
|
# Global read (no freq update to preserve isolation)
|
|
async with self.cache.lock:
|
|
node = self.cache.key_to_node.get(key)
|
|
if not node:
|
|
return None
|
|
# Lazy TTL check without mutation
|
|
if node.expires_at > 0 and node.expires_at <= time.time():
|
|
return None
|
|
return node.value
|
|
|
|
async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0):
|
|
"""Buffer a put operation without mutating global state."""
|
|
original = self.cache.key_to_node.get(key)
|
|
self.buffer.append({'op': 'put', 'key': key, 'value': value, 'ttl': ttl_seconds, 'original': original})
|
|
|
|
async def delete(self, key: Any):
|
|
"""Buffer a delete operation without mutating global state."""
|
|
original = self.cache.key_to_node.get(key)
|
|
self.buffer.append({'op': 'delete', 'key': key, 'original': original})
|
|
|
|
async def commit(self):
|
|
"""Apply buffered changes to global cache atomically."""
|
|
async with self.cache.lock:
|
|
# Merge buffer: keep only the latest operation per key
|
|
merged = {}
|
|
for op in self.buffer:
|
|
merged[op['key']] = op
|
|
|
|
for op in merged.values():
|
|
if op['op'] == 'put':
|
|
await self._apply_put(op['key'], op['value'], op['ttl'], self.cache.key_to_node.get(op['key']))
|
|
elif op['op'] == 'delete':
|
|
await self._apply_delete(op['key'], self.cache.key_to_node.get(op['key']))
|
|
|
|
self.buffer.clear()
|
|
self.committed = True
|
|
|
|
async def rollback(self):
|
|
"""Discard all pending changes without affecting global frequencies or TTL."""
|
|
self.buffer.clear()
|
|
self.committed = False
|
|
|
|
async def _apply_put(self, key: Any, value: Any, ttl: float, original_node: Optional[_Node]):
|
|
"""Apply buffered put to global cache."""
|
|
if original_node:
|
|
original_node.value = value
|
|
if ttl > 0:
|
|
original_node.expires_at = time.time() + ttl
|
|
self.cache._update_freq(original_node)
|
|
else:
|
|
if len(self.cache.key_to_node) >= self.cache.capacity:
|
|
self.cache._evict()
|
|
node = _Node(
|
|
key, value, freq=1,
|
|
expires_at=float('inf') if ttl <= 0 else time.time() + ttl
|
|
)
|
|
self.cache.key_to_node[key] = node
|
|
self.cache._add_to_freq(node)
|
|
self.cache.min_freq = 1
|
|
|
|
async def _apply_delete(self, key: Any, original_node: Optional[_Node]):
|
|
"""Apply buffered delete to global cache."""
|
|
if original_node:
|
|
self.cache._remove_node(original_node)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Executable Unit Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def main():
|
|
print("🧪 Running LFU Cache Test Suite...\n")
|
|
await test_lfu_eviction()
|
|
await test_ttl_eviction()
|
|
await test_transactions()
|
|
await test_stress()
|
|
print("✅ All tests passed successfully!")
|
|
|
|
async def test_lfu_eviction():
|
|
"""Verifies strict O(1) LFU eviction order when capacity is reached."""
|
|
cache = LFUCache(3)
|
|
await cache.put(1, 1)
|
|
await cache.put(2, 2)
|
|
await cache.put(3, 3)
|
|
|
|
# Access 1 -> freq becomes 2
|
|
assert await cache.get(1) == 1
|
|
|
|
# Insert 4 -> evicts key 2 (freq=1, LRU among freq=1)
|
|
await cache.put(4, 4)
|
|
|
|
assert await cache.get(2) is None, "Key 2 should be evicted"
|
|
assert await cache.get(1) == 1
|
|
assert await cache.get(3) == 3
|
|
assert await cache.get(4) == 4
|
|
print(" ✓ O(1) LFU eviction order verified")
|
|
|
|
async def test_ttl_eviction():
|
|
"""Verifies Lazy vs Background Async Sweep eviction."""
|
|
cache = LFUCache(10, evict_interval=0.1)
|
|
await cache.start_evictor()
|
|
|
|
# Lazy eviction on access
|
|
await cache.put('a', 1, ttl_seconds=0.2)
|
|
assert await cache.get('a') == 1
|
|
await asyncio.sleep(0.3)
|
|
assert await cache.get('a') is None, "Lazy eviction failed"
|
|
|
|
# Background async sweep
|
|
await cache.put('b', 2, ttl_seconds=0.1)
|
|
await asyncio.sleep(0.2) # Wait for background loop
|
|
assert await cache.get('b') is None, "Background sweep failed"
|
|
|
|
await cache.stop_evictor()
|
|
print(" ✓ Dual-layer TTL eviction (Lazy + Background) verified")
|
|
|
|
async def test_transactions():
|
|
"""Verifies Transaction commit visibility vs rollback state restoration."""
|
|
cache = LFUCache(5)
|
|
await cache.put('x', 10)
|
|
|
|
# Test 1: Isolation & Read-Your-Own-Writes
|
|
tx = await cache.begin_transaction()
|
|
await tx.put('x', 20)
|
|
assert await tx.get('x') == 20, "Transaction failed Read-Your-Own-Writes"
|
|
assert await cache.get('x') == 10, "Global cache leaked uncommitted write"
|
|
|
|
await tx.commit()
|
|
assert await cache.get('x') == 20, "Commit failed to apply changes"
|
|
|
|
# Test 2: Rollback restores state
|
|
tx2 = await cache.begin_transaction()
|
|
await tx2.put('y', 30)
|
|
await tx2.rollback()
|
|
assert await cache.get('y') is None, "Rollback failed to discard changes"
|
|
print(" ✓ Transaction commit/rollback isolation verified")
|
|
|
|
async def test_stress():
|
|
"""Stress test with 50 concurrent async tasks reading/writing simultaneously."""
|
|
cache = LFUCache(100)
|
|
await cache.start_evictor()
|
|
|
|
tasks = []
|
|
for i in range(50):
|
|
async def worker(idx):
|
|
tx = await cache.begin_transaction()
|
|
await tx.put(f'key_{idx}', idx)
|
|
await asyncio.sleep(0.01)
|
|
val = await tx.get(f'key_{idx}')
|
|
assert val == idx, f"Transaction read mismatch for key_{idx}"
|
|
await tx.commit()
|
|
# Verify global visibility post-commit
|
|
assert await cache.get(f'key_{idx}') == idx, f"Global read mismatch for key_{idx}"
|
|
tasks.append(asyncio.create_task(worker(i)))
|
|
|
|
await asyncio.gather(*tasks)
|
|
await cache.stop_evictor()
|
|
print(" ✓ Stress test (50 concurrent tasks) passed without race conditions")
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|
|
|