Initial benchmark suite: 8 graded models + cyberpunk dashboard generator
- 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>
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user