""" In-memory cache with TTL (L1 cache) Fast, limited size, lost on restart """ import time import logging from typing import Any, Optional, Dict from collections import OrderedDict logger = logging.getLogger(__name__) class MemoryCache: """ In-memory LRU cache with TTL support Features: - LRU eviction when max_size reached - Per-entry TTL expiration - Thread-safe operations - Fast O(1) get/set operations """ def __init__(self, max_size: int = 1000): """ Initialize memory cache Args: max_size: Maximum number of entries to store """ self.max_size = max_size self._cache: OrderedDict[str, Dict[str, Any]] = OrderedDict() self._stats = { 'hits': 0, 'misses': 0, 'sets': 0, 'evictions': 0 } async def get(self, key: str) -> Optional[Any]: """ Get value from cache Args: key: Cache key Returns: Cached value or None if not found/expired """ if key not in self._cache: self._stats['misses'] += 1 return None entry = self._cache[key] # Check TTL expiration if entry['expires_at'] < time.time(): # Expired - remove and return None del self._cache[key] self._stats['misses'] += 1 logger.debug(f"Memory cache expired: {key}") return None # Move to end (LRU - most recently used) self._cache.move_to_end(key) self._stats['hits'] += 1 logger.debug(f"Memory cache HIT: {key}") return entry['value'] async def set(self, key: str, value: Any, ttl: int): """ Set value in cache Args: key: Cache key value: Value to cache ttl: Time to live in seconds """ expires_at = time.time() + ttl # Check if we need to evict (LRU) if key not in self._cache and len(self._cache) >= self.max_size: # Evict oldest entry (first item in OrderedDict) evicted_key = next(iter(self._cache)) del self._cache[evicted_key] self._stats['evictions'] += 1 logger.debug(f"Memory cache evicted (LRU): {evicted_key}") # Store entry self._cache[key] = { 'value': value, 'expires_at': expires_at, 'created_at': time.time() } # Move to end (most recently used) self._cache.move_to_end(key) self._stats['sets'] += 1 logger.debug(f"Memory cache SET: {key} (TTL: {ttl}s)") async def delete(self, key: str) -> bool: """ Delete entry from cache Args: key: Cache key Returns: True if deleted, False if not found """ if key in self._cache: del self._cache[key] logger.debug(f"Memory cache deleted: {key}") return True return False async def clear(self): """Clear all entries from cache""" count = len(self._cache) self._cache.clear() logger.info(f"Memory cache cleared: {count} entries removed") async def clear_by_pattern(self, pattern: str): """ Clear entries matching pattern (simple prefix match) Args: pattern: Key prefix to match (e.g., "dashboard_summary:123") """ keys_to_delete = [key for key in self._cache.keys() if key.startswith(pattern)] for key in keys_to_delete: del self._cache[key] logger.info(f"Memory cache cleared by pattern '{pattern}': {len(keys_to_delete)} entries") async def cleanup_expired(self): """Remove all expired entries""" now = time.time() expired_keys = [ key for key, entry in self._cache.items() if entry['expires_at'] < now ] for key in expired_keys: del self._cache[key] if expired_keys: logger.info(f"Memory cache cleanup: {len(expired_keys)} expired entries removed") def get_stats(self) -> Dict[str, Any]: """ Get cache statistics Returns: Dictionary with stats (hits, misses, size, etc.) """ total_requests = self._stats['hits'] + self._stats['misses'] hit_rate = (self._stats['hits'] / total_requests * 100) if total_requests > 0 else 0 return { 'size': len(self._cache), 'max_size': self.max_size, 'hits': self._stats['hits'], 'misses': self._stats['misses'], 'sets': self._stats['sets'], 'evictions': self._stats['evictions'], 'hit_rate': hit_rate, 'total_requests': total_requests } def reset_stats(self): """Reset statistics counters""" self._stats = { 'hits': 0, 'misses': 0, 'sets': 0, 'evictions': 0 }