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>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user