""" In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions. Pure Python 3.11+ implementation using only built-in modules. """ import asyncio import time from dataclasses import dataclass from typing import Any, Optional, Dict, Tuple @dataclass class Node: """Represents a key-value pair in the cache with frequency and TTL tracking.""" key: Any value: Any freq: int = 1 expires_at: float = 0.0 prev: Optional['Node'] = None next: Optional['Node'] = None class DoublyLinkedList: """O(1) Doubly Linked List for maintaining insertion order within frequency buckets.""" def __init__(self) -> None: self.head = Node(key=None, value=None) self.tail = Node(key=None, value=None) self.head.next = self.tail self.tail.prev = self.head self.size = 0 def append(self, node: Node) -> None: """Append node to the tail (MRU position). O(1)""" node.prev = self.tail.prev node.next = self.tail self.tail.prev.next = node self.tail.prev = node self.size += 1 def remove(self, node: Node) -> None: """Remove specific node from the list. O(1)""" node.prev.next = node.next node.next.prev = node.prev node.prev = node.next = None self.size -= 1 def pop_lru(self) -> Optional[Node]: """Remove and return the LRU node (head.next). O(1)""" if self.size == 0: return None node = self.head.next self.remove(node) return node class Transaction: """ ACID-like transaction handle supporting Read-Your-Own-Writes isolation. Uncommitted changes are buffered locally and invisible to global readers. """ def __init__(self, cache: 'LFUCache') -> None: self._cache = cache # key -> (value, expires_at) | None (marks deletion) self._writes: Dict[Any, Optional[Tuple[Any, float]]] = {} self._committed = False self._rolled_back = False def _check_active(self) -> None: if self._committed or self._rolled_back: raise RuntimeError("Transaction is no longer active (already committed or rolled back).") async def get(self, key: Any) -> Any: """Read-Your-Own-Writes: checks local buffer first, then global cache.""" self._check_active() if key in self._writes and self._writes[key] is not None: val, exp = self._writes[key] return None if time.time() > exp else val return await self._cache.get(key) async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0) -> None: """Buffer write locally without mutating global state.""" self._check_active() exp = time.time() + ttl_seconds if ttl_seconds > 0 else float('inf') self._writes[key] = (value, exp) async def delete(self, key: Any) -> None: """Buffer deletion locally.""" self._check_active() self._writes[key] = None async def commit(self) -> None: """Apply buffered changes to the global cache atomically.""" self._check_active() async with self._cache.lock: for key, data in self._writes.items(): if data is None: # Apply deletion if key in self._cache.nodes: self._cache._remove_node(key) else: val, exp = data if key in self._cache.nodes: # Update existing node = self._cache.nodes[key] node.value = val node.expires_at = exp self._cache._update_freq(node) else: # Insert new if self._cache.size >= self._cache.capacity: self._cache._evict_lfu() node = Node(key=key, value=val, freq=1, expires_at=exp) self._cache.nodes[key] = node if 1 not in self._cache.freq_buckets: self._cache.freq_buckets[1] = DoublyLinkedList() self._cache.freq_buckets[1].append(node) self._cache.min_freq = 1 self._cache.size += 1 self._writes.clear() self._committed = True async def rollback(self) -> None: """Discard all pending changes without affecting global state.""" self._check_active() self._writes.clear() self._rolled_back = True class LFUCache: """ O(1) LFU Cache with Dual-Layer TTL Eviction and Async Concurrency. Uses frequency buckets + doubly linked lists for strict O(1) get/put. """ def __init__(self, capacity: int) -> None: if capacity <= 0: raise ValueError("Capacity must be a positive integer.") self.capacity = capacity self.nodes: Dict[Any, Node] = {} self.freq_buckets: Dict[int, DoublyLinkedList] = {} self.min_freq = 0 self.size = 0 self.lock = asyncio.Lock() self._evictor_task: Optional[asyncio.Task] = None self._running = False def begin_transaction(self) -> Transaction: """Start a new isolated transaction session.""" return Transaction(self) async def get(self, key: Any) -> Any: """Retrieve value by key. O(1) average time complexity.""" async with self.lock: if key not in self.nodes: return None node = self.nodes[key] # Lazy TTL Eviction if time.time() > node.expires_at: self._remove_node(key) return None self._update_freq(node) return node.value async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0) -> None: """Insert or update key-value pair. O(1) average time complexity.""" async with self.lock: if key in self.nodes: node = self.nodes[key] node.value = value node.expires_at = time.time() + ttl_seconds if ttl_seconds > 0 else float('inf') self._update_freq(node) return if self.size >= self.capacity: self._evict_lfu() node = Node( key=key, value=value, freq=1, expires_at=time.time() + ttl_seconds if ttl_seconds > 0 else float('inf') ) self.nodes[key] = node if 1 not in self.freq_buckets: self.freq_buckets[1] = DoublyLinkedList() self.freq_buckets[1].append(node) self.min_freq = 1 self.size += 1 def _update_freq(self, node: Node) -> None: """Move node to next frequency bucket. O(1)""" freq = node.freq if freq in self.freq_buckets: self.freq_buckets[freq].remove(node) if self.freq_buckets[freq].size == 0: del self.freq_buckets[freq] if self.min_freq == freq: self.min_freq += 1 node.freq += 1 if node.freq not in self.freq_buckets: self.freq_buckets[node.freq] = DoublyLinkedList() self.freq_buckets[node.freq].append(node) def _evict_lfu(self) -> None: """Evict LRU key from the minimum frequency bucket. O(1)""" if self.min_freq not in self.freq_buckets: return lru_node = self.freq_buckets[self.min_freq].pop_lru() if lru_node: del self.nodes[lru_node.key] if self.freq_buckets[self.min_freq].size == 0: del self.freq_buckets[self.min_freq] self.size -= 1 def _remove_node(self, key: Any) -> None: """Remove node from cache and frequency structure. O(1)""" node = self.nodes.pop(key) if node.freq in self.freq_buckets: self.freq_buckets[node.freq].remove(node) if self.freq_buckets[node.freq].size == 0: del self.freq_buckets[node.freq] if self.min_freq == node.freq: self.min_freq += 1 self.size -= 1 async def start_evictor(self, interval: float = 0.1, batch_size: int = 50) -> None: """Start background async TTL sweep task.""" if self._running: return self._running = True self._evictor_task = asyncio.create_task(self._evict_loop(interval, batch_size)) async def stop_evictor(self) -> None: """Gracefully stop the background evictor.""" self._running = False if self._evictor_task: self._evictor_task.cancel() try: await self._evictor_task except asyncio.CancelledError: pass async def _evict_loop(self, interval: float, batch_size: int) -> None: """Non-blocking background eviction that processes in small batches.""" while self._running: async with self.lock: now = time.time() checked = 0 # Snapshot keys to avoid RuntimeError during iteration/mutation for key in list(self.nodes.keys()): if checked >= batch_size: break node = self.nodes.get(key) if node and now > node.expires_at: self._remove_node(key) checked += 1 await asyncio.sleep(interval) # ============================================================================= # EXECUTABLE UNIT TESTS # ============================================================================= async def main() -> None: print("=== Running LFU Cache Test Suite ===\n") # a) O(1) LFU eviction order print("[TEST a] LFU Eviction Order...") cache = LFUCache(3) await cache.put('a', 1) await cache.put('b', 2) await cache.put('c', 3) await cache.get('a') # freq: a=2, b=1, c=1 await cache.put('d', 4) # Evicts 'b' (LRU among freq=1) assert await cache.get('b') is None, "LFU eviction failed: 'b' should be evicted" assert await cache.get('a') == 1 assert await cache.get('c') == 3 assert await cache.get('d') == 4 print(" ✅ PASSED\n") # b) Lazy TTL vs Background Async Sweep print("[TEST b] Dual-Layer TTL Eviction...") cache2 = LFUCache(10) await cache2.start_evictor(interval=0.05, batch_size=10) # Lazy eviction test await cache2.put('lazy', 'val', ttl_seconds=0.1) await asyncio.sleep(0.12) assert await cache2.get('lazy') is None, "Lazy TTL eviction failed" # Background sweep test await cache2.put('bg1', 'v', ttl_seconds=0.05) await cache2.put('bg2', 'v', ttl_seconds=0.05) await asyncio.sleep(0.12) assert 'bg1' not in cache2.nodes, "Background sweep failed to remove 'bg1'" assert 'bg2' not in cache2.nodes, "Background sweep failed to remove 'bg2'" await cache2.stop_evictor() print(" ✅ PASSED\n") # c) Transaction commit visibility vs rollback state restoration print("[TEST c] Transaction Isolation & Rollback...") cache3 = LFUCache(10) # Commit test tx1 = cache3.begin_transaction() await tx1.put('x', 100) assert await cache3.get('x') is None, "Isolation broken: global reader saw uncommitted write" assert await tx1.get('x') == 100, "Read-Your-Own-Writes failed" await tx1.commit() assert await cache3.get('x') == 100, "Commit failed: value not visible globally" # Rollback test tx2 = cache3.begin_transaction() await tx2.put('y', 200) await tx2.rollback() assert await cache3.get('y') is None, "Rollback failed: uncommitted value leaked" print(" ✅ PASSED\n") # d) Stress test: 50 concurrent async tasks print("[TEST d] Concurrency Stress Test (50 tasks, 100 ops each)...") cache4 = LFUCache(50) async def worker(wid: int) -> None: for i in range(100): key = f"k_{wid}_{i}" op = i % 3 if op == 0: await cache4.put(key, f"v_{i}", ttl_seconds=0.5 if i % 2 == 0 else 0) elif op == 1: await cache4.get(key) else: tx = cache4.begin_transaction() await tx.put(key, f"tx_{i}") if i % 2 == 0: await tx.commit() else: await tx.rollback() tasks = [asyncio.create_task(worker(i)) for i in range(50)] await asyncio.gather(*tasks) assert cache4.size <= 50, f"Capacity violation during stress test: size={cache4.size}" print(" ✅ PASSED\n") print("=== All Tests Passed Successfully ===") if __name__ == "__main__": asyncio.run(main())