patristic-be / src /api /cache.py
Mario33333's picture
deploy: reader-access security hardening (feature/audiobook@06a5ed8)
7c2d250 verified
Raw
History Blame Contribute Delete
2.56 kB
"""Tiny in-process TTL cache for read-path responses.
The cloud backend talks to Turso / Qdrant / R2 across regions; every call is
an HTTP round-trip. The read endpoints (book list, book detail, status
counts) are recomputed on every navigation even though the underlying data
barely changes between clicks β€” that's the bulk of the "moving between pages
is slow" latency the owner reported.
A short-TTL in-memory cache collapses repeat reads to a single process-local
lookup. The HF Space runs a single replica, so a process-local dict is
correct; user-facing writes (create / patch / delete book, label edits)
invalidate by key or key-prefix so a read never serves data the same user
just changed. The TTL is the backstop for changes that happen outside the
request path (e.g. a pipeline job flipping a book's status) β€” those become
visible within ``DEFAULT_TTL`` seconds.
Keep it dumb: no LRU eviction (the key space is tiny β€” a handful of books Γ—
filter combinations), no async, no cross-process coherence.
"""
from __future__ import annotations
import threading
import time
from typing import Callable, TypeVar
DEFAULT_TTL = 30.0 # seconds
_T = TypeVar("_T")
class _Entry:
__slots__ = ("value", "expires")
def __init__(self, value: object, expires: float) -> None:
self.value = value
self.expires = expires
_store: dict[str, _Entry] = {}
_lock = threading.Lock()
def get_or_set(key: str, producer: Callable[[], _T], *, ttl: float = DEFAULT_TTL) -> _T:
"""Return the cached value for ``key`` or compute, store, and return it.
``producer`` runs OUTSIDE the lock so concurrent misses don't serialize
behind one another's DB calls (a brief duplicate-compute on a cold key is
cheaper than holding the lock across a cross-region round-trip).
"""
now = time.monotonic()
with _lock:
e = _store.get(key)
if e is not None and e.expires > now:
return e.value # type: ignore[return-value]
value = producer()
with _lock:
_store[key] = _Entry(value, time.monotonic() + ttl)
return value
def invalidate(key: str) -> None:
with _lock:
_store.pop(key, None)
def invalidate_prefix(prefix: str) -> None:
with _lock:
for k in [k for k in _store if k.startswith(prefix)]:
_store.pop(k, None)
def clear() -> None:
"""Drop everything β€” test hook + a hard reset for cache-coherence bugs."""
with _lock:
_store.clear()