From 53cdd63b6a11646f05060c68a83839f25ae0e00b Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 14 Apr 2026 12:14:50 +0200 Subject: [PATCH] Add no-op session cache when session cache is disabled Use NoOpSessionCache in backend/app/main.py and dynamically switch cache implementation in backend/app/dependencies.py so disabled cache mode remains safe while get_session_cache always returns a valid object. --- Docs/Tasks.md | 2 ++ backend/app/dependencies.py | 15 +++++++++++++-- backend/app/main.py | 8 ++++++-- backend/app/utils/session_cache.py | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 6d21909..f2db24d 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -667,6 +667,8 @@ A named 16-worker thread pool that is never actually used consumes OS thread res ### Task 23 — Fix InMemorySessionCache instantiated when disabled +**Status:** Completed + **Severity:** Low **Where:** diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 22e591b..2e590f3 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -26,7 +26,7 @@ from app.repositories.protocols import SessionRepository from app.services.protocols import AuthService, JailService from app.utils.constants import SESSION_COOKIE_NAME from app.utils.runtime_state import RuntimeState -from app.utils.session_cache import SessionCache +from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache, SessionCache log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -79,6 +79,17 @@ def _session_cache_enabled(settings: Settings) -> bool: def _build_app_context(request: Request) -> ApplicationContext: state = cast("AppState", request.app.state) + session_cache = getattr(state, "session_cache", None) + if session_cache is None: + session_cache = NoOpSessionCache() + state.session_cache = session_cache + elif _session_cache_enabled(state.settings) and isinstance(session_cache, NoOpSessionCache): + session_cache = InMemorySessionCache() + state.session_cache = session_cache + elif not _session_cache_enabled(state.settings) and not isinstance(session_cache, NoOpSessionCache): + session_cache = NoOpSessionCache() + state.session_cache = session_cache + return ApplicationContext( settings=state.settings, http_session=getattr(state, "http_session", None), @@ -88,7 +99,7 @@ def _build_app_context(request: Request) -> ApplicationContext: last_activation=getattr(state, "last_activation", None), runtime_settings=getattr(state, "runtime_settings", None), runtime_state=state.runtime_state, - session_cache=getattr(state, "session_cache", None), + session_cache=session_cache, ) diff --git a/backend/app/main.py b/backend/app/main.py index f5d6700..323b3f4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -46,7 +46,7 @@ from app.routers import ( from app.startup import startup_shared_resources from app.utils.fail2ban_client import Fail2BanConnectionError, Fail2BanProtocolError from app.utils.runtime_state import ApplicationState, RuntimeState -from app.utils.session_cache import InMemorySessionCache +from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache from app.utils.setup_state import is_setup_complete_cached, set_setup_complete_cache log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -290,7 +290,11 @@ def create_app(settings: Settings | None = None) -> FastAPI: # shared Starlette state bag itself does not hold mutable business state. app.state = ApplicationState(RuntimeState()) app.state.settings = resolved_settings - app.state.session_cache = InMemorySessionCache() + app.state.session_cache = ( + InMemorySessionCache() + if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0 + else NoOpSessionCache() + ) set_setup_complete_cache(app, False) # --- CORS --- diff --git a/backend/app/utils/session_cache.py b/backend/app/utils/session_cache.py index 6c06675..5d63c66 100644 --- a/backend/app/utils/session_cache.py +++ b/backend/app/utils/session_cache.py @@ -55,3 +55,19 @@ class InMemorySessionCache: def clear(self) -> None: self._entries.clear() + + +class NoOpSessionCache: + """A no-op session cache used when caching is disabled.""" + + def get(self, token: str) -> Session | None: + return None + + def set(self, token: str, session: Session, ttl_seconds: float) -> None: + return None + + def invalidate(self, token: str) -> None: + return None + + def clear(self) -> None: + return None