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,398 @@
|
||||
"""
|
||||
In-Memory Concurrent LFU Cache with Async TTL Eviction & Atomic Transactions.
|
||||
Pure Python 3.11+ implementation using asyncio, dataclasses, and collections.OrderedDict.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Set, Tuple
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Node:
|
||||
"""Internal node storing cache value and TTL expiry timestamp."""
|
||||
key: str
|
||||
value: Any
|
||||
ttl_expiry: float
|
||||
|
||||
|
||||
class LFUCache:
|
||||
"""
|
||||
O(1) Least Frequently Used Cache with dual-layer TTL eviction and async concurrency.
|
||||
|
||||
Data Structures:
|
||||
- self.cache: Dict[str, _Node] -> Direct O(1) key lookup
|
||||
- self.freq_map: Dict[int, OrderedDict[str, None]] -> Frequency buckets maintaining insertion order (LRU within same freq)
|
||||
- self.key_freq: Dict[str, int] -> Tracks current frequency of each key for O(1) updates
|
||||
- self.ttl_map: Dict[str, float] -> Stores absolute TTL expiry timestamps
|
||||
"""
|
||||
|
||||
def __init__(self, capacity: int):
|
||||
self.capacity = max(0, capacity)
|
||||
self.cache: Dict[str, _Node] = {}
|
||||
self.freq_map: Dict[int, OrderedDict[str, None]] = {}
|
||||
self.key_freq: Dict[str, int] = {}
|
||||
self.ttl_map: Dict[str, float] = {}
|
||||
self.min_freq: int = 0
|
||||
self._lock = asyncio.Lock()
|
||||
self._evictor_task: Optional[asyncio.Task] = None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# PUBLIC API #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def start_evictor(self) -> None:
|
||||
"""Start the non-blocking background TTL eviction loop."""
|
||||
if self._evictor_task is not None:
|
||||
return
|
||||
self._evictor_task = asyncio.create_task(self._background_loop())
|
||||
|
||||
async def stop_evictor(self) -> None:
|
||||
"""Gracefully stop the background eviction task."""
|
||||
if self._evictor_task:
|
||||
self._evictor_task.cancel()
|
||||
try:
|
||||
await self._evictor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._evictor_task = None
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
Retrieve value by key. O(1) average time complexity.
|
||||
Performs lazy TTL eviction upon access.
|
||||
"""
|
||||
current_time = time.monotonic()
|
||||
expiry = self.ttl_map.get(key)
|
||||
|
||||
# Lazy TTL Eviction
|
||||
if expiry is not None and current_time >= expiry:
|
||||
await self._remove_key(key)
|
||||
return None
|
||||
|
||||
async with self._lock:
|
||||
if key not in self.cache:
|
||||
return None
|
||||
|
||||
node = self.cache[key]
|
||||
|
||||
# Frequency Increment & Bucket Migration (O(1))
|
||||
old_freq = self.key_freq[key]
|
||||
new_freq = old_freq + 1
|
||||
self.key_freq[key] = new_freq
|
||||
|
||||
old_bucket = self.freq_map[old_freq]
|
||||
del old_bucket[key]
|
||||
if not old_bucket:
|
||||
del self.freq_map[old_freq]
|
||||
if self.min_freq == old_freq:
|
||||
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
|
||||
|
||||
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
|
||||
new_bucket[key] = None # Store key reference in bucket
|
||||
|
||||
return node.value
|
||||
|
||||
async def put(self, key: str, value: Any, ttl_seconds: float) -> None:
|
||||
"""
|
||||
Insert or update key-value pair with TTL. O(1) average time complexity.
|
||||
Performs lazy TTL eviction before insertion if needed.
|
||||
"""
|
||||
current_time = time.monotonic()
|
||||
expiry = self.ttl_map.get(key)
|
||||
|
||||
# Lazy TTL Eviction for stale keys
|
||||
if expiry is not None and current_time >= expiry:
|
||||
await self._remove_key(key)
|
||||
|
||||
async with self._lock:
|
||||
if key in self.cache:
|
||||
# Update existing: increment frequency & migrate bucket
|
||||
old_freq = self.key_freq[key]
|
||||
new_freq = old_freq + 1
|
||||
self.key_freq[key] = new_freq
|
||||
|
||||
old_bucket = self.freq_map[old_freq]
|
||||
del old_bucket[key]
|
||||
if not old_bucket:
|
||||
del self.freq_map[old_freq]
|
||||
if self.min_freq == old_freq:
|
||||
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
|
||||
|
||||
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
|
||||
new_bucket[key] = None
|
||||
else:
|
||||
# Insert new: evict LFU if at capacity
|
||||
if len(self.cache) >= self.capacity and self.capacity > 0:
|
||||
await self._evict_lfu()
|
||||
|
||||
freq = 1
|
||||
self.key_freq[key] = freq
|
||||
bucket = self.freq_map.setdefault(freq, OrderedDict())
|
||||
bucket[key] = None
|
||||
self.min_freq = 1
|
||||
|
||||
# Update node & TTL map
|
||||
self.cache[key] = _Node(key=key, value=value, ttl_expiry=current_time + ttl_seconds)
|
||||
self.ttl_map[key] = current_time + ttl_seconds
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete a key from the cache. O(1)."""
|
||||
async with self._lock:
|
||||
if key not in self.cache:
|
||||
return False
|
||||
await self._remove_key(key)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# INTERNAL HELPERS #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def _evict_lfu(self) -> None:
|
||||
"""Evict the least frequently used key (oldest among ties). O(1)."""
|
||||
if not self.freq_map or self.min_freq == 0:
|
||||
return
|
||||
|
||||
# Ensure min_freq points to a valid tier
|
||||
while self.min_freq in self.freq_map and self.min_freq < max(self.freq_map.keys()):
|
||||
pass
|
||||
if self.min_freq not in self.freq_map:
|
||||
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
|
||||
|
||||
bucket = self.freq_map[self.min_freq]
|
||||
evict_key, _ = bucket.popitem(last=False)
|
||||
await self._remove_key(evict_key)
|
||||
|
||||
async def _remove_key(self, key: str) -> None:
|
||||
"""Remove key from all internal structures. O(1)."""
|
||||
if key not in self.cache:
|
||||
return
|
||||
|
||||
node = self.cache.pop(key)
|
||||
self.ttl_map.pop(key, None)
|
||||
|
||||
freq = self.key_freq.pop(key)
|
||||
bucket = self.freq_map[freq]
|
||||
del bucket[key]
|
||||
|
||||
if not bucket:
|
||||
del self.freq_map[freq]
|
||||
if self.min_freq == freq:
|
||||
# Find next valid minimum frequency
|
||||
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
|
||||
|
||||
async def _background_loop(self) -> None:
|
||||
"""Non-blocking background task that purges expired keys in batches."""
|
||||
while True:
|
||||
await asyncio.sleep(0.1) # Check interval
|
||||
current_time = time.monotonic()
|
||||
|
||||
async with self._lock:
|
||||
# Collect expired keys safely
|
||||
expired_keys = [k for k, exp in list(self.ttl_map.items()) if current_time >= exp]
|
||||
|
||||
# Purge in batch (yields control between removals implicitly via await)
|
||||
for key in expired_keys:
|
||||
await self._remove_key(key)
|
||||
|
||||
async def apply_transaction(self, tx: "Transaction") -> None:
|
||||
"""Atomically apply transaction buffers to global state."""
|
||||
async with self._lock:
|
||||
# 1. Apply local deletes first (frees capacity for puts)
|
||||
for key in list(tx._local_deletes):
|
||||
if key in self.cache:
|
||||
await self._remove_key(key)
|
||||
tx._local_puts.pop(key, None)
|
||||
|
||||
# 2. Apply local puts
|
||||
for key, (value, expiry) in tx._local_puts.items():
|
||||
if key in self.cache:
|
||||
old_freq = self.key_freq[key]
|
||||
new_freq = old_freq + 1
|
||||
self.key_freq[key] = new_freq
|
||||
|
||||
old_bucket = self.freq_map[old_freq]
|
||||
del old_bucket[key]
|
||||
if not old_bucket:
|
||||
del self.freq_map[old_freq]
|
||||
if self.min_freq == old_freq:
|
||||
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
|
||||
|
||||
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
|
||||
new_bucket[key] = None
|
||||
else:
|
||||
if len(self.cache) >= self.capacity and self.capacity > 0:
|
||||
await self._evict_lfu()
|
||||
|
||||
freq = 1
|
||||
self.key_freq[key] = freq
|
||||
bucket = self.freq_map.setdefault(freq, OrderedDict())
|
||||
bucket[key] = None
|
||||
self.min_freq = 1
|
||||
|
||||
self.cache[key] = _Node(key=key, value=value, ttl_expiry=expiry)
|
||||
self.ttl_map[key] = expiry
|
||||
|
||||
|
||||
class Transaction:
|
||||
"""
|
||||
ACID-like sub-session handle supporting Read-Your-Own-Writes and isolation.
|
||||
Global readers do not see uncommitted writes until commit().
|
||||
"""
|
||||
|
||||
def __init__(self, cache: LFUCache):
|
||||
self.cache = cache
|
||||
self._local_puts: Dict[str, Tuple[Any, float]] = {} # key -> (value, absolute_expiry)
|
||||
self._local_deletes: Set[str] = set()
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
"""Read with local buffer priority (Read-Your-Own-Writes)."""
|
||||
if key in self._local_deletes:
|
||||
return None
|
||||
if key in self._local_puts:
|
||||
val, _ = self._local_puts[key]
|
||||
return val
|
||||
# Fall back to global cache (handles lazy eviction & lock)
|
||||
return await self.cache.get(key)
|
||||
|
||||
async def put(self, key: str, value: Any, ttl_seconds: float) -> None:
|
||||
"""Buffer write locally. Does not affect global state until commit."""
|
||||
current_time = time.monotonic()
|
||||
self._local_puts[key] = (value, current_time + ttl_seconds)
|
||||
if key in self._local_deletes:
|
||||
self._local_deletes.remove(key)
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Buffer deletion locally."""
|
||||
self._local_deletes.add(key)
|
||||
self._local_puts.pop(key, None)
|
||||
|
||||
async def commit(self) -> None:
|
||||
"""Atomically apply all buffered changes to the global cache."""
|
||||
await self.cache.apply_transaction(self)
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""Discard all pending local changes without mutating global state."""
|
||||
self._local_puts.clear()
|
||||
self._local_deletes.clear()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# TEST SUITE #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def main():
|
||||
print("🧪 Starting LFU Cache Test Suite...\n")
|
||||
|
||||
# a) O(1) LFU Eviction Order
|
||||
print("[a] Testing O(1) LFU Eviction Order...")
|
||||
cache = LFUCache(capacity=3)
|
||||
await cache.put("A", 1, ttl_seconds=60)
|
||||
await cache.put("B", 2, ttl_seconds=60)
|
||||
await cache.put("C", 3, ttl_seconds=60)
|
||||
|
||||
# Access A twice to increase its frequency
|
||||
await cache.get("A")
|
||||
await cache.get("A")
|
||||
|
||||
# Insert D. Should evict B or C (both freq=1). LFU policy guarantees one of them is gone.
|
||||
await cache.put("D", 4, ttl_seconds=60)
|
||||
|
||||
val_b = await cache.get("B")
|
||||
val_c = await cache.get("C")
|
||||
assert val_d := await cache.get("D"), "D should exist"
|
||||
assert val_a := await cache.get("A"), "A should exist (highest freq)"
|
||||
assert val_b is None or val_c is None, f"LFU eviction failed: B={val_b}, C={val_c}"
|
||||
print(f" ✅ LFU Eviction verified. Evicted key had lower frequency than A & D.")
|
||||
|
||||
# b) Lazy TTL vs Background Async Sweep
|
||||
print("\n[b] Testing Dual-Layer TTL Eviction...")
|
||||
cache2 = LFUCache(capacity=10)
|
||||
|
||||
# Lazy Eviction Test
|
||||
await cache2.put("lazy_key", "val", ttl_seconds=0.2)
|
||||
assert await cache2.get("lazy_key") == "val"
|
||||
await asyncio.sleep(0.3)
|
||||
assert await cache2.get("lazy_key") is None, "Lazy eviction failed"
|
||||
|
||||
# Background Eviction Test
|
||||
await cache2.start_evictor()
|
||||
await cache2.put("bg_key", "val", ttl_seconds=0.15)
|
||||
await asyncio.sleep(0.3) # Wait past TTL without accessing key
|
||||
assert await cache2.get("bg_key") is None, "Background async sweep failed"
|
||||
await cache2.stop_evictor()
|
||||
print(" ✅ Lazy & Background TTL eviction verified.")
|
||||
|
||||
# c) Transaction Commit Visibility vs Rollback
|
||||
print("\n[c] Testing Transaction Isolation & Rollback...")
|
||||
cache3 = LFUCache(capacity=10)
|
||||
|
||||
tx1 = cache3.begin_transaction() if hasattr(cache3, 'begin_transaction') else None
|
||||
class TxWrapper:
|
||||
def __init__(self, c): self.c = c
|
||||
def begin(self): return Transaction(self.c)
|
||||
tw = TxWrapper(cache3)
|
||||
|
||||
# Commit visibility
|
||||
tx_put = tw.begin()
|
||||
await tx_put.put("committed", 100, ttl_seconds=60)
|
||||
assert await cache3.get("committed") is None, "Uncommitted write should be invisible"
|
||||
await tx_put.commit()
|
||||
assert await cache3.get("committed") == 100, "Committed write should be visible globally"
|
||||
|
||||
# Rollback state restoration
|
||||
tx_roll = tw.begin()
|
||||
await tx_roll.put("rolled_back", 200, ttl_seconds=60)
|
||||
await tx_roll.rollback()
|
||||
assert await cache3.get("rolled_back") is None, "Rolled back write should not persist"
|
||||
|
||||
# Read-Your-Own-Writes inside transaction
|
||||
tx_ryo = tw.begin()
|
||||
await tx_ryo.put("local", 999, ttl_seconds=60)
|
||||
assert await tx_ryo.get("local") == 999, "Transaction should see its own writes"
|
||||
print(" ✅ Transaction commit visibility & rollback verified.")
|
||||
|
||||
# d) Stress Test: 50 Concurrent Tasks
|
||||
print("\n[d] Running Stress Test (50 concurrent async tasks)...")
|
||||
cache4 = LFUCache(capacity=100)
|
||||
await cache4.start_evictor()
|
||||
|
||||
errors = []
|
||||
results = {"gets": 0, "puts": 0}
|
||||
|
||||
async def worker(task_id: int):
|
||||
try:
|
||||
for i in range(20):
|
||||
key = f"stress_{task_id}_{i}"
|
||||
op_type = i % 3
|
||||
|
||||
if op_type == 0:
|
||||
await cache4.put(key, f"v_{task_id}_{i}", ttl_seconds=1.0)
|
||||
results["puts"] += 1
|
||||
elif op_type == 1:
|
||||
val = await cache4.get(key)
|
||||
if val is not None:
|
||||
results["gets"] += 1
|
||||
else:
|
||||
tx = Transaction(cache4)
|
||||
await tx.put(f"tx_{task_id}_{i}", "tx_val", ttl_seconds=0.5)
|
||||
assert await tx.get(f"tx_{task_id}_{i}") == "tx_val"
|
||||
await tx.commit()
|
||||
except Exception as e:
|
||||
errors.append((task_id, str(e)))
|
||||
|
||||
tasks = [asyncio.create_task(worker(i)) for i in range(50)]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
assert len(errors) == 0, f"Stress test failed with errors: {errors}"
|
||||
print(f" ✅ Stress test passed. Processed {results['puts']} puts & {results['gets']} gets across 50 tasks without race conditions.")
|
||||
await cache4.stop_evictor()
|
||||
|
||||
print("\n🎉 All tests passed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user