Files
hackathon-acapulquitos-boys…/server/app/core/cache.py

198 lines
6.4 KiB
Python

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