""" 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())