"""Session cleanup background task. Registers an APScheduler job that periodically removes expired sessions from the ``sessions`` table. Without this cleanup, the table grows unbounded over months of operation and degrades query performance. Individual expired sessions are removed on-demand when validated, but the bulk cleanup ensures comprehensive pruning at a predictable interval. """ from __future__ import annotations from typing import TYPE_CHECKING import structlog from app.repositories import session_repo from app.tasks.db import task_db from app.tasks.timeout_utils import run_with_timeout from app.utils.runtime_state import get_effective_settings from app.utils.time_utils import utc_now if TYPE_CHECKING: from fastapi import FastAPI from app.config import Settings log: structlog.stdlib.BoundLogger = structlog.get_logger() #: How often the cleanup job fires (seconds). Configurable tuning constant. SESSION_CLEANUP_INTERVAL: int = 6 * 60 * 60 # 6 hours #: Stable APScheduler job ID — ensures re-registration replaces, not duplicates. JOB_ID: str = "session_cleanup" #: Maximum seconds to allow for session cleanup to complete. TASK_TIMEOUT_SECONDS: int = 30 async def _run_cleanup_with_resources(settings: Settings) -> None: """Delete all expired sessions from the database. Args: settings: The resolved application settings used for database access. """ async def _do_cleanup() -> None: now_iso = utc_now().isoformat() async with task_db(settings) as db: deleted_count = await session_repo.delete_expired_sessions(db, now_iso) log.info("session_cleanup_ran", deleted_count=deleted_count, cutoff_time=now_iso) await run_with_timeout("session_cleanup", _do_cleanup(), TASK_TIMEOUT_SECONDS) async def _run_cleanup(app: FastAPI) -> None: await _run_cleanup_with_resources(get_effective_settings(app)) def register(app: FastAPI) -> None: """Add (or replace) the session cleanup job in the application scheduler. Must be called after the scheduler has been started (i.e., inside the lifespan handler, after ``scheduler.start()``). Args: app: The :class:`fastapi.FastAPI` application instance whose ``app.state.scheduler`` will receive the job. """ settings = get_effective_settings(app) app.state.scheduler.add_job( _run_cleanup_with_resources, trigger="interval", seconds=SESSION_CLEANUP_INTERVAL, kwargs={"settings": settings}, id=JOB_ID, replace_existing=True, ) log.info("session_cleanup_scheduled", interval_seconds=SESSION_CLEANUP_INTERVAL)