198 lines
6.4 KiB
Python
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}") |