Actualizar backend/cache_manager.py
This commit is contained in:
@@ -8,6 +8,34 @@ _CACHE: Dict[str, Tuple[float, Any]] = {}
|
||||
_LOCK = threading.Lock()
|
||||
_DEFAULT_TTL = 1800 # 30 min
|
||||
|
||||
# ── Lock POR CLAVE (evita "estampida"): si varias peticiones piden el MISMO dato
|
||||
# vencido al mismo tiempo, solo la PRIMERA ejecuta la consulta pesada; las demas
|
||||
# esperan ese resultado en vez de lanzar su propia copia (que saturaba la base y
|
||||
# hacia que todo se sintiera colgado). Cada clave tiene su propio candado.
|
||||
_KEY_LOCKS: Dict[str, threading.Lock] = {}
|
||||
_KEY_LOCKS_GUARD = threading.Lock()
|
||||
|
||||
def _lock_de(key: str) -> threading.Lock:
|
||||
with _KEY_LOCKS_GUARD:
|
||||
lk = _KEY_LOCKS.get(key)
|
||||
if lk is None:
|
||||
lk = threading.Lock()
|
||||
_KEY_LOCKS[key] = lk
|
||||
return lk
|
||||
|
||||
# ── Modo "recarga en 2do plano" ────────────────────────────────────────────
|
||||
# Mientras esta activo (solo lo activa el refresco de los 30 min, en su hilo),
|
||||
# cache_get_or_set RECALCULA cada dato con info fresca y recien al terminar
|
||||
# reemplaza la entrada. Asi la cache nunca queda vacia: el usuario sigue viendo
|
||||
# los datos viejos hasta que TODO lo nuevo esta listo (swap = 1 parpadeo).
|
||||
_FORZAR = threading.local()
|
||||
|
||||
def _forzar_activo() -> bool:
|
||||
return getattr(_FORZAR, "on", False)
|
||||
|
||||
def set_forzar(on: bool) -> None:
|
||||
_FORZAR.on = on
|
||||
|
||||
|
||||
def _make_key(prefix: str, args: tuple) -> str:
|
||||
return prefix + ":" + "|".join(str(a) for a in args)
|
||||
@@ -16,13 +44,29 @@ def _make_key(prefix: str, args: tuple) -> str:
|
||||
def cache_get_or_set(prefix: str, args: tuple, loader: Callable[[], Any], ttl: int = _DEFAULT_TTL) -> Any:
|
||||
key = _make_key(prefix, args)
|
||||
now = time.time()
|
||||
if _forzar_activo():
|
||||
# Refresco en 2do plano: recalcula y sobrescribe SIN pasar por el lock de
|
||||
# usuario (no debe esperar a nadie ni bloquear a los que leen la cache).
|
||||
value = loader()
|
||||
with _LOCK:
|
||||
_CACHE[key] = (time.time(), value)
|
||||
return value
|
||||
|
||||
# Lectura normal: si esta fresco, devolver YA.
|
||||
with _LOCK:
|
||||
hit = _CACHE.get(key)
|
||||
if hit and (now - hit[0] < ttl):
|
||||
return hit[1]
|
||||
|
||||
# Vencido/ausente: solo UNA peticion calcula; las demas esperan (lock x clave).
|
||||
with _lock_de(key):
|
||||
with _LOCK:
|
||||
hit = _CACHE.get(key)
|
||||
if hit and (time.time() - hit[0] < ttl):
|
||||
return hit[1]
|
||||
value = loader()
|
||||
with _LOCK:
|
||||
_CACHE[key] = (now, value)
|
||||
_CACHE[key] = (time.time(), value)
|
||||
return value
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user