Disable session cache by default and make it opt-in for single-process deployments
This commit is contained in:
@@ -45,6 +45,21 @@ class Settings(BaseSettings):
|
||||
ge=1,
|
||||
description="Number of minutes a session token remains valid after creation.",
|
||||
)
|
||||
session_cache_enabled: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Enable the in-memory session validation cache. "
|
||||
"Disable it in multi-worker deployments to avoid stale revoked sessions."
|
||||
),
|
||||
)
|
||||
session_cache_ttl_seconds: float = Field(
|
||||
default=10.0,
|
||||
ge=0.0,
|
||||
description=(
|
||||
"How long (seconds) a cached session validation entry remains fresh. "
|
||||
"Ignored when session_cache_enabled is false."
|
||||
),
|
||||
)
|
||||
timezone: str = Field(
|
||||
default="UTC",
|
||||
description="IANA timezone name used when displaying timestamps in the UI.",
|
||||
|
||||
@@ -50,8 +50,6 @@ _COOKIE_NAME = "bangui_session"
|
||||
#: NOTE: this cache is process-local and is not cluster-safe. In multi-worker
|
||||
#: or distributed deployments, each process maintains its own cache, so logout
|
||||
#: invalidation and revocation may be delayed unless a shared cache is used.
|
||||
_SESSION_CACHE_TTL: float = 10.0
|
||||
|
||||
#: ``token → (Session, cache_expiry_monotonic_time)``
|
||||
_session_cache: dict[str, tuple[Session, float]] = {}
|
||||
|
||||
@@ -65,6 +63,11 @@ def clear_session_cache() -> None:
|
||||
_session_cache.clear()
|
||||
|
||||
|
||||
def _session_cache_enabled(settings: Settings) -> bool:
|
||||
"""Return whether the in-memory session cache should be used."""
|
||||
return settings.session_cache_enabled and settings.session_cache_ttl_seconds > 0.0
|
||||
|
||||
|
||||
def invalidate_session_cache(token: str) -> None:
|
||||
"""Evict *token* from the in-memory session cache.
|
||||
|
||||
@@ -213,10 +216,12 @@ async def require_auth(
|
||||
The token is read from the ``bangui_session`` cookie or the
|
||||
``Authorization: Bearer`` header.
|
||||
|
||||
Validated tokens are cached in memory for :data:`_SESSION_CACHE_TTL`
|
||||
seconds so that concurrent requests sharing the same token avoid repeated
|
||||
SQLite round-trips. The cache is bypassed on expiry and explicitly
|
||||
cleared by :func:`invalidate_session_cache` on logout.
|
||||
Validated tokens may be cached in memory for a short period so that
|
||||
concurrent requests sharing the same token avoid repeated SQLite
|
||||
round-trips. This cache is disabled by default because process-local
|
||||
invalidation is not safe in multi-worker or clustered deployments.
|
||||
When enabled, entries are bypassed on expiry and explicitly cleared by
|
||||
:func:`invalidate_session_cache` on logout.
|
||||
|
||||
Args:
|
||||
request: The incoming FastAPI request.
|
||||
@@ -244,15 +249,17 @@ async def require_auth(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Fast path: serve from in-memory cache when the entry is still fresh and
|
||||
# the session itself has not yet exceeded its own expiry time.
|
||||
cached = _session_cache.get(token)
|
||||
if cached is not None:
|
||||
session, cache_expires_at = cached
|
||||
if time.monotonic() < cache_expires_at and session.expires_at > utc_now().isoformat():
|
||||
return session
|
||||
# Stale cache entry — evict and fall through to DB.
|
||||
_session_cache.pop(token, None)
|
||||
cache_enabled = _session_cache_enabled(settings)
|
||||
if cache_enabled:
|
||||
# Fast path: serve from in-memory cache when the entry is still fresh and
|
||||
# the session itself has not yet exceeded its own expiry time.
|
||||
cached = _session_cache.get(token)
|
||||
if cached is not None:
|
||||
session, cache_expires_at = cached
|
||||
if time.monotonic() < cache_expires_at and session.expires_at > utc_now().isoformat():
|
||||
return session
|
||||
# Stale cache entry — evict and fall through to DB.
|
||||
_session_cache.pop(token, None)
|
||||
|
||||
try:
|
||||
session = await auth_service.validate_session(db, token, settings.session_secret)
|
||||
@@ -263,7 +270,11 @@ async def require_auth(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
) from exc
|
||||
|
||||
_session_cache[token] = (session, time.monotonic() + _SESSION_CACHE_TTL)
|
||||
if cache_enabled:
|
||||
_session_cache[token] = (
|
||||
session,
|
||||
time.monotonic() + settings.session_cache_ttl_seconds,
|
||||
)
|
||||
return session
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user