""" Sistema de caching con Redis y memoria """ import json import hashlib import logging from functools import wraps from typing import Optional, Any, Callable from datetime import datetime, timedelta import redis from redis.exceptions import RedisError from app.core.config import settings logger = logging.getLogger(__name__) class CacheClient: """Cliente unificado de caché""" def __init__(self): self.redis_client = None self.memory_cache = {} # Fallback en memoria self.enabled = settings.cache_enabled if self.enabled: try: self.redis_client = redis.Redis( host=settings.redis_host, port=settings.redis_port, db=settings.redis_db, password=settings.redis_password, decode_responses=True, socket_connect_timeout=5, socket_timeout=5 ) # Probar conexión self.redis_client.ping() logger.info(f"✅ Redis conectado en {settings.redis_host}:{settings.redis_port}") except RedisError as e: logger.warning(f"⚠️ Redis no disponible: {e}. Usando caché en memoria.") self.redis_client = None self.enabled = False def _get_key(self, prefix: str, key: str) -> str: """Genera clave con prefijo""" return f"{prefix}:{key}" def _hash_key(self, key: str) -> str: """Hash de claves largas""" if len(key) > 100: return hashlib.md5(key.encode()).hexdigest() return key async def get(self, prefix: str, key: str) -> Optional[Any]: """Obtener valor del caché""" if not self.enabled: return None cache_key = self._get_key(prefix, self._hash_key(key)) try: # Intentar Redis primero if self.redis_client: value = self.redis_client.get(cache_key) if value: return json.loads(value) # Fallback a memoria if cache_key in self.memory_cache: data, expiry = self.memory_cache[cache_key] if datetime.now() < expiry: return data else: del self.memory_cache[cache_key] return None except Exception as e: logger.error(f"Error reading cache: {e}") return None async def set(self, prefix: str, key: str, value: Any, ttl: int = 60): """Guardar en caché""" if not self.enabled: return cache_key = self._get_key(prefix, self._hash_key(key)) try: serialized = json.dumps(value, default=str) if self.redis_client: self.redis_client.setex(cache_key, ttl, serialized) # Guardar también en memoria self.memory_cache[cache_key] = (value, datetime.now() + timedelta(seconds=ttl)) except Exception as e: logger.error(f"Error writing cache: {e}") async def delete(self, prefix: str, key: str): """Eliminar del caché""" if not self.enabled: return cache_key = self._get_key(prefix, self._hash_key(key)) try: if self.redis_client: self.redis_client.delete(cache_key) if cache_key in self.memory_cache: del self.memory_cache[cache_key] except Exception as e: logger.error(f"Error deleting cache: {e}") async def delete_pattern(self, pattern: str): """Eliminar por patrón""" if not self.enabled or not self.redis_client: return try: keys = self.redis_client.keys(pattern) if keys: self.redis_client.delete(*keys) except Exception as e: logger.error(f"Error deleting pattern: {e}") async def clear_all(self): """Limpiar todo el caché""" if not self.enabled: return try: if self.redis_client: self.redis_client.flushdb() self.memory_cache.clear() logger.info("Cache cleared") except Exception as e: logger.error(f"Error clearing cache: {e}") # Singleton cache_client = CacheClient() def cached(prefix: str, ttl: int = 60, key_builder: Optional[Callable] = None): """ Decorador para cachear respuestas de endpoints Uso: @cached(prefix="eta", ttl=30) async def get_eta(address_id: int): ... """ def decorator(func: Callable): @wraps(func) async def wrapper(*args, **kwargs): if not cache_client.enabled: return await func(*args, **kwargs) # Construir clave if key_builder: cache_key = key_builder(*args, **kwargs) else: # Usar nombre de función y argumentos cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}" # Intentar obtener del caché cached_value = await cache_client.get(prefix, cache_key) if cached_value is not None: logger.debug(f"Cache HIT: {prefix}:{cache_key}") return cached_value # Ejecutar función result = await func(*args, **kwargs) # Guardar en caché if result is not None: await cache_client.set(prefix, cache_key, result, ttl) logger.debug(f"Cache MISS: {prefix}:{cache_key} saved") return result return wrapper return decorator async def invalidate_user_cache(user_id: int): """Invalidar caché relacionada con un usuario""" await cache_client.delete_pattern(f"addresses:user:{user_id}:*") await cache_client.delete_pattern(f"eta:user:{user_id}:*") logger.info(f"Cache invalidated for user {user_id}") async def invalidate_route_cache(route_id: str): """Invalidar caché relacionada con una ruta""" await cache_client.delete_pattern(f"eta:route:{route_id}:*") await cache_client.delete_pattern(f"truck_status:{route_id}") logger.info(f"Cache invalidated for route {route_id}")