From e57d19fd76e5022e071fe477d8daf126c407384d Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 25 Apr 2026 18:23:08 +0200 Subject: [PATCH] T-05: Remove app.state mutation from _build_app_context Move session cache initialization from per-request _build_app_context to startup lifespan handler. The session cache type is now decided once at app startup based on settings, making _build_app_context pure (read-only). Changes: - Move cache initialization logic to new _update_session_cache() in main.py - Call _update_session_cache() during lifespan startup to initialize cache - Remove three if/elif/elif branches mutating state.session_cache from _build_app_context - Add cache swap logic to set_runtime_settings() in runtime_state.py to handle runtime settings changes (e.g., setup wizard updates) - Keep app.state.session_cache initialization in create_app() for test compatibility This ensures: - _build_app_context is pure and doesn't mutate app state on each request - Session cache configuration decisions are centralized at startup - Settings changes during runtime (via setup wizard) also trigger cache swap - Cache initialization logic is isolated in one place Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/app/dependencies.py | 11 ++--------- backend/app/main.py | 22 ++++++++++++++++++++++ backend/app/utils/runtime_state.py | 18 +++++++++++++++++- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 7d69898..d037258 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -28,13 +28,13 @@ from app.repositories.protocols import ( GeoCacheRepository, HistoryArchiveRepository, ImportLogRepository, - SettingsRepository, SessionRepository, + SettingsRepository, ) from app.services.geo_cache import GeoCache from app.utils.constants import SESSION_COOKIE_NAME from app.utils.runtime_state import RuntimeState -from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache, SessionCache +from app.utils.session_cache import NoOpSessionCache, SessionCache log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -91,13 +91,6 @@ def _build_app_context(request: Request) -> ApplicationContext: 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, diff --git a/backend/app/main.py b/backend/app/main.py index ae51e19..a50a9f6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -117,6 +117,23 @@ def _configure_logging(log_level: str) -> None: # --------------------------------------------------------------------------- +def _update_session_cache(app: FastAPI, settings: Settings) -> None: + """Update the session cache backend based on settings. + + Replaces the current cache with InMemorySessionCache or NoOpSessionCache + depending on whether session caching is enabled and configured with a + positive TTL. + + Args: + app: The :class:`fastapi.FastAPI` instance. + settings: The effective application settings. + """ + cache_enabled = settings.session_cache_enabled and settings.session_cache_ttl_seconds > 0.0 + app.state.session_cache = ( + InMemorySessionCache() if cache_enabled else NoOpSessionCache() + ) + + @asynccontextmanager async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Manage the lifetime of all shared application resources. @@ -137,6 +154,11 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app.state.http_session = http_session app.state.scheduler = scheduler + # Ensure session cache is initialized based on effective settings. + # This cache is process-local and not cluster-safe. In multi-worker + # deployments, it should be replaced with a shared backend. + _update_session_cache(app, settings) + log.info("bangui_started") try: diff --git a/backend/app/utils/runtime_state.py b/backend/app/utils/runtime_state.py index 2f10865..95c60ff 100644 --- a/backend/app/utils/runtime_state.py +++ b/backend/app/utils/runtime_state.py @@ -24,6 +24,7 @@ from starlette.datastructures import State from app.models.config import PendingRecovery from app.models.server import ServerStatus +from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache if TYPE_CHECKING: # pragma: no cover from app.config import Settings @@ -118,10 +119,25 @@ def get_effective_settings(app: Any) -> Settings: def set_runtime_settings(app: Any, settings: Settings) -> None: - """Store the resolved runtime settings separately from bootstrap config.""" + """Store the resolved runtime settings separately from bootstrap config. + + Also updates the session cache backend if the session cache configuration + has changed, replacing it with InMemorySessionCache or NoOpSessionCache + as appropriate. + + Args: + app: The FastAPI application instance. + settings: The new effective settings. + """ runtime_state = get_runtime_state(app) runtime_state.runtime_settings = settings + # Update session cache if settings changed + cache_enabled = settings.session_cache_enabled and settings.session_cache_ttl_seconds > 0.0 + new_cache = InMemorySessionCache() if cache_enabled else NoOpSessionCache() + app.state.session_cache = new_cache + log.debug("session_cache_updated", cache_type=type(new_cache).__name__) + def update_app_settings(app: Any, **overrides: Any) -> None: """Update the current effective settings immutably."""