Convert setup guard to startup-driven cache and update tests

This commit is contained in:
2026-04-06 20:38:15 +02:00
parent 3ccfc20c64
commit 89ab41cc9e
5 changed files with 109 additions and 59 deletions

View File

@@ -50,6 +50,10 @@ from app.routers import (
from app.tasks import blocklist_import, geo_cache_flush, geo_re_resolve, health_check, history_sync
from app.utils.fail2ban_client import Fail2BanConnectionError, Fail2BanProtocolError
from app.utils.jail_config import ensure_jail_configs
from app.utils.setup_state import (
is_setup_complete_cached,
set_setup_complete_cache,
)
log: structlog.stdlib.BoundLogger = structlog.get_logger()
@@ -122,6 +126,11 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await init_db(db)
await geo_service.load_cache_from_db(db)
unresolved_count = await geo_service.count_unresolved(db)
from app.services import setup_service # noqa: PLC0415
setup_complete = await setup_service.is_setup_complete(db)
set_setup_complete_cache(app, setup_complete)
log.debug("setup_completion_cached", completed=setup_complete)
finally:
await db.close()
@@ -133,8 +142,6 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.http_session = http_session
# --- Pre-warm geo cache from the persistent store ---
from app.services import geo_service # noqa: PLC0415
geo_service.init_geoip(settings.geoip_db_path)
# --- Background task scheduler ---
@@ -292,39 +299,14 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
return await call_next(request)
# If setup is not complete, block all other API requests.
# Fast path: setup completion is a one-way transition. Once it is
# True it is cached on app.state so all subsequent requests skip the
# DB query entirely. The flag is reset only when the app restarts.
if path.startswith("/api") and not getattr(
request.app.state, "_setup_complete_cached", False
):
from app.db import open_db # noqa: PLC0415
from app.services import setup_service # noqa: PLC0415
db = getattr(request.app.state, "db", None)
if db is None:
settings = request.app.state.settings
db = await open_db(settings.database_path)
try:
is_complete = await setup_service.is_setup_complete(db)
except Exception:
log.debug("setup_check_failed", reason="db_uninitialised_or_inaccessible")
is_complete = False
finally:
await db.close()
else:
try:
is_complete = await setup_service.is_setup_complete(db)
except Exception:
log.debug("setup_check_failed", reason="db_uninitialised_or_inaccessible")
is_complete = False
if not is_complete:
return RedirectResponse(
url="/api/setup",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
request.app.state._setup_complete_cached = True
# The setup completion state is resolved at startup and stored in
# ``app.state.setup_complete_cached`` so this middleware does not
# perform any database queries during normal request handling.
if path.startswith("/api") and not is_setup_complete_cached(request.app):
return RedirectResponse(
url="/api/setup",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
return await call_next(request)
@@ -360,6 +342,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
# Store settings on app.state so the lifespan handler can access them.
app.state.settings = resolved_settings
set_setup_complete_cache(app, False)
# --- CORS ---
# In production the frontend is served by the same origin.