# app/utils/cache_manager.py """ Cache Manager ============= Implementa un semplice LRU cache per query database ripetute. Features: - LRU (Least Recently Used) eviction policy - TTL (Time To Live) per entry scadute - Thread-safe operations - Cache invalidation per dashboard """ import time import hashlib import json import threading from typing import Any, Optional, Callable from collections import OrderedDict from loguru import logger class CacheManager: """ Gestore cache con LRU e TTL. Usage: cache = CacheManager(max_size=100, default_ttl=300) # Cache una query key = cache.generate_key('dashboard_tipologie', filters_dict) cached_data = cache.get(key) if cached_data is None: # Esegui query data = execute_query(filters_dict) cache.set(key, data) else: data = cached_data # Invalidate cache per un dashboard cache.invalidate_namespace('dashboard_tipologie') """ def __init__(self, max_size: int = 100, default_ttl: int = 300): """ Inizializza il CacheManager. Args: max_size: Numero massimo di entry in cache default_ttl: TTL di default in secondi (300 = 5 minuti) """ self.max_size = max_size self.default_ttl = default_ttl self._cache = OrderedDict() # {key: (value, timestamp, ttl)} self._lock = threading.Lock() logger.info(f"CacheManager inizializzato: max_size={max_size}, default_ttl={default_ttl}s") def generate_key(self, namespace: str, params: dict) -> str: """ Genera una chiave univoca per una query. Args: namespace: Namespace (es. 'dashboard_tipologie', 'dashboard_diritti') params: Parametri della query Returns: Chiave hash univoca """ # Serializza params in modo deterministico params_str = json.dumps(params, sort_keys=True) params_hash = hashlib.md5(params_str.encode()).hexdigest() key = f"{namespace}:{params_hash}" return key def get(self, key: str) -> Optional[Any]: """ Ottiene un valore dalla cache. Args: key: Chiave della entry Returns: Valore cached oppure None se non trovato o scaduto """ with self._lock: if key not in self._cache: logger.debug(f"Cache MISS: {key}") return None value, timestamp, ttl = self._cache[key] current_time = time.time() # Check TTL if current_time - timestamp > ttl: logger.debug(f"Cache EXPIRED: {key}") del self._cache[key] return None # Move to end (LRU) self._cache.move_to_end(key) logger.debug(f"Cache HIT: {key}") return value def set(self, key: str, value: Any, ttl: Optional[int] = None): """ Salva un valore in cache. Args: key: Chiave della entry value: Valore da cachare ttl: TTL custom (se None usa default) """ with self._lock: if ttl is None: ttl = self.default_ttl # Evict se necessario if key not in self._cache and len(self._cache) >= self.max_size: # Remove oldest (first item in OrderedDict) oldest_key = next(iter(self._cache)) logger.debug(f"Cache EVICT (LRU): {oldest_key}") del self._cache[oldest_key] # Store self._cache[key] = (value, time.time(), ttl) # Move to end self._cache.move_to_end(key) logger.debug(f"Cache SET: {key} (TTL={ttl}s)") def invalidate(self, key: str): """ Invalida una singola entry. Args: key: Chiave da invalidare """ with self._lock: if key in self._cache: del self._cache[key] logger.debug(f"Cache INVALIDATE: {key}") def invalidate_namespace(self, namespace: str): """ Invalida tutte le entry di un namespace. Args: namespace: Namespace da invalidare (es. 'dashboard_tipologie') """ with self._lock: keys_to_delete = [k for k in self._cache.keys() if k.startswith(f"{namespace}:")] for key in keys_to_delete: del self._cache[key] if keys_to_delete: logger.info(f"Cache INVALIDATE namespace '{namespace}': {len(keys_to_delete)} entries rimossi") def clear(self): """Pulisce completamente la cache""" with self._lock: count = len(self._cache) self._cache.clear() logger.info(f"Cache CLEAR: {count} entries rimossi") def get_stats(self) -> dict: """ Ottiene statistiche sulla cache. Returns: Dict con statistiche """ with self._lock: return { 'size': len(self._cache), 'max_size': self.max_size, 'utilization': f"{len(self._cache) / self.max_size * 100:.1f}%", 'keys': list(self._cache.keys()) } def cached_query(self, namespace: str, params: dict, query_func: Callable, ttl: Optional[int] = None) -> Any: """ Utility method per eseguire una query con caching automatico. Args: namespace: Namespace della cache params: Parametri della query query_func: Funzione che esegue la query (callable) ttl: TTL custom Returns: Risultato della query (da cache o freshly executed) Example: def load_data(filters): return database.execute_query(filters) data = cache.cached_query( 'dashboard_tipologie', filters_dict, lambda: load_data(filters_dict), ttl=600 ) """ key = self.generate_key(namespace, params) # Try cache first cached_value = self.get(key) if cached_value is not None: return cached_value # Execute query logger.debug(f"Cache MISS, executing query: {namespace}") value = query_func() # Store in cache self.set(key, value, ttl) return value # ========== SINGLETON INSTANCE ========== _cache_manager_instance: Optional[CacheManager] = None def get_cache_manager() -> CacheManager: """ Ottiene l'istanza singleton del CacheManager. Returns: CacheManager instance """ global _cache_manager_instance if _cache_manager_instance is None: _cache_manager_instance = CacheManager( max_size=100, # Max 100 query cached default_ttl=300 # 5 minuti di default ) return _cache_manager_instance