- Extraccion de helpers en backend: audit_helpers.py, helpers.py - Modularizacion de schemas en archivos individuales por dominio - Reduccion de audit.py en 953 lineas (74% del archivo) - Reduccion de tickets.py en 655 lineas (60% del archivo) - Expansion de auth.py con recuperacion de contrasenia y tokens - Nuevos modulos: core/email.py, core/cache.py - Reorganizacion de scripts a backend/scripts/ - Frontend: refactorizacion de audit page con array-driven components - Frontend: correccion de 11 errores ortograficos en tickets page - Frontend: proxy Docker corregido en vite.config.js - Frontend: nuevas rutas forgot-password, reset-password, organization, profile - Nuevas utilidades TS: colorUtils.ts, dateFormats.ts - 5 nuevos archivos de tests unitarios en backend/tests/unit/ - Eliminacion de 3 scripts temporales de prueba - Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
309 lines
8.8 KiB
Python
309 lines
8.8 KiB
Python
"""
|
|
Redis Caching Service - ServiceManagerWeb
|
|
|
|
Servicio centralizado para manejo de caché con Redis.
|
|
"""
|
|
|
|
from redis import asyncio as aioredis
|
|
from typing import Optional, Any, Union
|
|
import json
|
|
import structlog
|
|
from functools import wraps
|
|
|
|
from app.core.config import get_settings
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
settings = get_settings()
|
|
|
|
|
|
class CacheService:
|
|
"""
|
|
Servicio de caché usando Redis.
|
|
|
|
Proporciona métodos para get/set/delete de datos con serialización JSON.
|
|
Usa un singleton pattern para compartir la conexión Redis.
|
|
"""
|
|
|
|
_instance = None
|
|
_redis = None
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
return cls._instance
|
|
|
|
async def connect(self):
|
|
"""Conectar a Redis si aún no está conectado."""
|
|
if self._redis is None:
|
|
try:
|
|
self._redis = await aioredis.from_url(
|
|
settings.REDIS_URL,
|
|
encoding="utf-8",
|
|
decode_responses=True,
|
|
socket_connect_timeout=5,
|
|
socket_timeout=5
|
|
)
|
|
logger.info("Redis cache connected", url=settings.REDIS_URL)
|
|
except Exception as e:
|
|
logger.error("Failed to connect to Redis", error=str(e))
|
|
self._redis = None
|
|
|
|
async def disconnect(self):
|
|
"""Cerrar conexión Redis."""
|
|
if self._redis:
|
|
await self._redis.close()
|
|
self._redis = None
|
|
logger.info("Redis cache disconnected")
|
|
|
|
async def get(self, key: str) -> Optional[Any]:
|
|
"""
|
|
Obtener valor del cache.
|
|
|
|
Args:
|
|
key: Clave del cache
|
|
|
|
Returns:
|
|
Valor deserializado o None si no existe
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
logger.warning("Redis not available, skipping cache get", key=key)
|
|
return None
|
|
|
|
try:
|
|
value = await self._redis.get(key)
|
|
if value:
|
|
logger.debug("Cache hit", key=key)
|
|
return json.loads(value)
|
|
logger.debug("Cache miss", key=key)
|
|
return None
|
|
except Exception as e:
|
|
logger.error("Cache get error", key=key, error=str(e))
|
|
return None
|
|
|
|
async def set(
|
|
self,
|
|
key: str,
|
|
value: Any,
|
|
ttl: int = 300
|
|
) -> bool:
|
|
"""
|
|
Guardar valor en cache.
|
|
|
|
Args:
|
|
key: Clave del cache
|
|
value: Valor a guardar (será serializado a JSON)
|
|
ttl: Tiempo de vida en segundos (default: 5 minutos)
|
|
|
|
Returns:
|
|
True si se guardó exitosamente
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
logger.warning("Redis not available, skipping cache set", key=key)
|
|
return False
|
|
|
|
try:
|
|
serialized = json.dumps(value, default=str)
|
|
await self._redis.setex(key, ttl, serialized)
|
|
logger.debug("Cache set", key=key, ttl=ttl)
|
|
return True
|
|
except Exception as e:
|
|
logger.error("Cache set error", key=key, error=str(e))
|
|
return False
|
|
|
|
async def delete(self, key: str) -> bool:
|
|
"""
|
|
Eliminar clave del cache.
|
|
|
|
Args:
|
|
key: Clave a eliminar
|
|
|
|
Returns:
|
|
True si se eliminó exitosamente
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
logger.warning("Redis not available, skipping cache delete", key=key)
|
|
return False
|
|
|
|
try:
|
|
await self._redis.delete(key)
|
|
logger.debug("Cache delete", key=key)
|
|
return True
|
|
except Exception as e:
|
|
logger.error("Cache delete error", key=key, error=str(e))
|
|
return False
|
|
|
|
async def delete_pattern(self, pattern: str) -> int:
|
|
"""
|
|
Eliminar todas las claves que coincidan con el patrón.
|
|
|
|
Args:
|
|
pattern: Patrón de búsqueda (ej: "tickets:tenant:*")
|
|
|
|
Returns:
|
|
Número de claves eliminadas
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
logger.warning("Redis not available, skipping pattern delete", pattern=pattern)
|
|
return 0
|
|
|
|
try:
|
|
keys = []
|
|
async for key in self._redis.scan_iter(pattern):
|
|
keys.append(key)
|
|
|
|
if keys:
|
|
deleted = await self._redis.delete(*keys)
|
|
logger.info("Cache pattern delete", pattern=pattern, deleted=deleted)
|
|
return deleted
|
|
return 0
|
|
except Exception as e:
|
|
logger.error("Cache pattern delete error", pattern=pattern, error=str(e))
|
|
return 0
|
|
|
|
async def exists(self, key: str) -> bool:
|
|
"""
|
|
Verificar si una clave existe en cache.
|
|
|
|
Args:
|
|
key: Clave a verificar
|
|
|
|
Returns:
|
|
True si existe
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
return False
|
|
|
|
try:
|
|
return await self._redis.exists(key) > 0
|
|
except Exception as e:
|
|
logger.error("Cache exists error", key=key, error=str(e))
|
|
return False
|
|
|
|
async def incr(self, key: str, amount: int = 1) -> Optional[int]:
|
|
"""
|
|
Incrementar un contador en cache.
|
|
|
|
Args:
|
|
key: Clave del contador
|
|
amount: Cantidad a incrementar
|
|
|
|
Returns:
|
|
Nuevo valor del contador
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
return None
|
|
|
|
try:
|
|
return await self._redis.incrby(key, amount)
|
|
except Exception as e:
|
|
logger.error("Cache incr error", key=key, error=str(e))
|
|
return None
|
|
|
|
async def expire(self, key: str, ttl: int) -> bool:
|
|
"""
|
|
Establecer tiempo de expiración a una clave existente.
|
|
|
|
Args:
|
|
key: Clave a expirar
|
|
ttl: Tiempo de vida en segundos
|
|
|
|
Returns:
|
|
True si se estableció exitosamente
|
|
"""
|
|
if self._redis is None:
|
|
await self.connect()
|
|
|
|
if self._redis is None:
|
|
return False
|
|
|
|
try:
|
|
return await self._redis.expire(key, ttl)
|
|
except Exception as e:
|
|
logger.error("Cache expire error", key=key, error=str(e))
|
|
return False
|
|
|
|
|
|
# Singleton instance
|
|
cache = CacheService()
|
|
|
|
|
|
def cache_key(*parts: str) -> str:
|
|
"""
|
|
Helper para construir claves de cache consistentes.
|
|
|
|
Args:
|
|
*parts: Partes de la clave a unir
|
|
|
|
Returns:
|
|
Clave formateada
|
|
|
|
Example:
|
|
cache_key("tickets", "tenant", tenant_id) -> "tickets:tenant:123"
|
|
"""
|
|
return ":".join(str(part) for part in parts)
|
|
|
|
|
|
def cached(
|
|
key_prefix: str,
|
|
ttl: int = 300,
|
|
key_builder: Optional[callable] = None
|
|
):
|
|
"""
|
|
Decorator para cachear resultados de funciones async.
|
|
|
|
Args:
|
|
key_prefix: Prefijo para la clave de cache
|
|
ttl: Tiempo de vida en segundos
|
|
key_builder: Función opcional para construir la clave
|
|
|
|
Example:
|
|
@cached("categories", ttl=600)
|
|
async def get_categories(tenant_id: str):
|
|
return await db.query(Category).all()
|
|
"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
# Construir clave de cache
|
|
if key_builder:
|
|
key = key_builder(*args, **kwargs)
|
|
else:
|
|
# Default: usar nombre de función y args
|
|
key_parts = [key_prefix, func.__name__]
|
|
key_parts.extend(str(arg) for arg in args)
|
|
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
|
key = cache_key(*key_parts)
|
|
|
|
# Intentar obtener del cache
|
|
cached_value = await cache.get(key)
|
|
if cached_value is not None:
|
|
return cached_value
|
|
|
|
# Si no está en cache, ejecutar función
|
|
result = await func(*args, **kwargs)
|
|
|
|
# Guardar en cache
|
|
await cache.set(key, result, ttl=ttl)
|
|
|
|
return result
|
|
return wrapper
|
|
return decorator
|