import asyncio import time import weakref from dataclasses import dataclass, field from typing import Optional, Dict, Any, List, Set, Tuple from collections import OrderedDict import threading # --- Data Structures --- @dataclass class CacheNode: """Doubly-linked node for LFU frequency buckets.""" key: str value: Any freq: int = 0 ttl: Optional[float] = None # Unix timestamp of expiry prev: Optional['CacheNode'] = None next: Optional['CacheNode'] = None @dataclass class FrequencyBucket: """A bucket of nodes with the same frequency.""" freq: int head: CacheNode = field(default_factory=lambda: CacheNode("", None, 0)) # Dummy head tail: CacheNode = field(default_factory=lambda: CacheNode("", None, 0)) # Dummy tail nodes: Dict[str, CacheNode] = field(default_factory=dict) def __post_init__(self): self.head.next = self.tail self.tail.prev = self.head def add_node(self, node: CacheNode): """Add a node to the front of the bucket.""" node.next = self.head.next node.prev = self.head self.head.next.prev = node self.head.next = node self.nodes[node.key] = node def remove_node(self, node: CacheNode): """Remove a node from the bucket.""" node.prev.next = node.next node.next.prev = node.prev del self.nodes[node.key] def is_empty(self) -> bool: return len(self.nodes) == 0 # --- Transaction Support --- class Transaction: def __init__(self, cache: 'InMemoryLFUCache'): self.cache = cache self._writes: Dict[str, Tuple[Any, Optional[float]]] = {} # key -> (value, ttl) self._deletions: Set[str] = set() self._committed = False self._rolled_back = False def get(self, key: str) -> Any: if self._rolled_back or self._committed: raise RuntimeError("Transaction is no longer active") if key in self._deletions: return None if key in self._writes: return self._writes[key][0] return self.cache.get(key) def put(self, key: str, value: Any, ttl_seconds: Optional[float] = None): if self._rolled_back or self._committed: raise RuntimeError("Transaction is no longer active") self._writes[key] = (value, ttl_seconds) def delete(self, key: str): if self._rolled_back or self._committed: raise RuntimeError("Transaction is no longer active") self._deletions.add(key) async def commit(self): if self._rolled_back or self._committed: raise RuntimeError("Transaction is no longer active") await self.cache._commit_transaction(self) self._committed = True async def rollback(self): if self._rolled_back or self._committed: raise RuntimeError("Transaction is no longer active") self._rolled_back = True # --- Cache Implementation --- class InMemoryLFUCache: def __init__(self, capacity: int = 100): self.capacity = capacity self._data: Dict[str, CacheNode] = {} # Global cache data self._freq_buckets: Dict[int, FrequencyBucket] = {} self._min_freq = 0 self._lock = asyncio.Lock() self._evictor_task: Optional[asyncio.Task] = None self._evictor_running = False self._transaction_lock = asyncio.Lock() self._transactions: Dict[int, Transaction] = {} self._transaction_counter = 0 def _create_bucket(self, freq: int) -> FrequencyBucket: bucket = FrequencyBucket(freq) self._freq_buckets[freq] = bucket return bucket def _get_bucket(self, freq: int) -> FrequencyBucket: if freq not in self._freq_buckets: return self._create_bucket(freq) return self._freq_buckets[freq] def _evict_lfu(self): """Evict the least frequently used item.""" if self._min_freq not in self._freq_buckets: return None bucket = self._freq_buckets[self._min_freq] if bucket.is_empty(): return None node_to_evict = bucket.tail.prev # Tail's previous is the last item self._remove_node(node_to_evict) return node_to_evict.key def _remove_node(self, node: CacheNode): """Remove a node from its bucket and global map.""" bucket = self._get_bucket(node.freq) bucket.remove_node(node) if bucket.is_empty(): del self._freq_buckets[node.freq] del self._data[node.key] def _update_node_frequency(self, node: CacheNode): """Move node to the next frequency bucket and update min_freq if needed.""" old_freq = node.freq node.freq += 1 new_freq = node.freq bucket = self._get_bucket(old_freq) bucket.remove_node(node) if bucket.is_empty(): del self._freq_buckets[old_freq] if old_freq == self._min_freq: self._min_freq = new_freq new_bucket = self._get_bucket(new_freq) new_bucket.add_node(node) def _is_expired(self, node: CacheNode) -> bool: if node.ttl is None: return False return time.time() > node.ttl def _check_and_remove_expired(self, node: CacheNode) -> bool: """Returns True if node was expired and removed.""" if self._is_expired(node): self._remove_node(node) return True return False async def get(self, key: str) -> Any: async with self._lock: if key not in self._data: return None node = self._data[key] # Lazy TTL check if self._check_and_remove_expired(node): return None self._update_node_frequency(node) return node.value async def put(self, key: str, value: Any, ttl_seconds: Optional[float] = None): async with self._lock: ttl = time.time() + ttl_seconds if ttl_seconds is not None else None if key in self._data: node = self._data[key] # Lazy TTL check if self._check_and_remove_expired(node): node = None # Re-add it below if node: node.value = value node.ttl = ttl self._update_node_frequency(node) return # Add new node or replace existing expired one node = CacheNode(key, value, freq=1, ttl=ttl) self._data[key] = node bucket = self._get_bucket(1) bucket.add_node(node) self._min_freq = 1 # Check capacity and evict if needed if len(self._data) > self.capacity: evicted_key = self._evict_lfu() if evicted_key is not None: # Ensure that the bucket is cleaned up if empty pass def begin_transaction(self) -> Transaction: with self._transaction_lock: self._transaction_counter += 1 tx = Transaction(self) self._transactions[self._transaction_counter] = tx return tx async def _commit_transaction(self, tx: Transaction): async with self._lock: # Apply writes for key, (value, ttl) in tx._writes.items(): await self.put(key, value, ttl) # Apply deletions for key in tx._deletions: if key in self._data: node = self._data[key] self._remove_node(node) async def start_evictor(self, interval_seconds: float = 5.0): """Start the background evictor task.""" async def _evict_loop(): while self._evictor_running: try: await asyncio.sleep(interval_seconds) await self._evict_expired_batch() except Exception: pass # Silently ignore errors in background task self._evictor_running = True self._evictor_task = asyncio.create_task(_evict_loop()) async def stop_evictor(self): """Stop the background evictor task.""" self._evictor_running = False if self._evictor_task: self._evictor_task.cancel() try: await self._evictor_task except asyncio.CancelledError: pass async def _evict_expired_batch(self, batch_size: int = 10): """Evict a batch of expired keys without holding the global lock for too long.""" to_remove = [] with self._lock: # Collect expired nodes for node in self._data.values(): if self._is_expired(node): to_remove.append(node.key) if len(to_remove) >= batch_size: break # Remove outside lock to avoid blocking readers for key in to_remove: async with self._lock: if key in self._data: node = self._data[key] if self._is_expired(node): self._remove_node(node) # --- Unit Tests --- async def main(): print("Running InMemoryLFUCache tests...") # Test 1: O(1) LFU eviction print("Test 1: LFU eviction") cache = InMemoryLFUCache(capacity=3) await cache.put("a", 1) await cache.put("b", 2) await cache.put("c", 3) # Access a twice to make it more frequent await cache.get("a") await cache.get("a") # Add d, should evict the least frequent (b) await cache.put("d", 4) assert await cache.get("b") is None assert await cache.get("a") == 1 assert await cache.get("c") == 3 assert await cache.get("d") == 4 print("✓ LFU eviction works") # Test 2: TTL eviction print("Test 2: TTL eviction") cache = InMemoryLFUCache(capacity=10) await cache.put("a", 1, ttl_seconds=0.1) # Expire quickly await asyncio.sleep(0.2) assert await cache.get("a") is None print("✓ TTL eviction works") # Test 3: Transaction commit visibility print("Test 3: Transaction visibility") cache = InMemoryLFUCache(capacity=10) tx = cache.begin_transaction() tx.put("x", 100) assert await cache.get("x") is None # Not committed yet await tx.commit() assert await cache.get("x") == 100 # Now visible print("✓ Transaction commit works") # Test 4: Transaction rollback print("Test 4: Transaction rollback") cache = InMemoryLFUCache(capacity=10) await cache.put("y", 200) tx = cache.begin_transaction() tx.put("y", 300) tx.delete("z") # z doesn't exist yet await tx.rollback() assert await cache.get("y") == 200 # Original value restored print("✓ Transaction rollback works") # Test 5: Concurrent access print("Test 5: Concurrent access") cache = InMemoryLFUCache(capacity=10) async def worker(worker_id: int): for i in range(5): key = f"{worker_id}_{i}" await cache.put(key, i) val = await cache.get(key) assert val == i tasks = [asyncio.create_task(worker(i)) for i in range(50)] await asyncio.gather(*tasks) print("✓ Concurrent access works") # Test 6: Background eviction print("Test 6: Background eviction") cache = InMemoryLFUCache(capacity=10) await cache.put("expiring", 1, ttl_seconds=0.1) await cache.start_evictor(interval_seconds=0.05) await asyncio.sleep(0.2) assert await cache.get("expiring") is None await cache.stop_evictor() print("✓ Background eviction works") print("\nAll tests passed! ✅") if __name__ == "__main__": asyncio.run(main())