"""History sync background task. Periodically copies new records from the fail2ban sqlite database into the BanGUI application archive table to prevent gaps when fail2ban purges old rows. Correlation IDs are propagated through the task using :mod:`app.utils.correlation` so that task logs can be correlated across runs. """ from __future__ import annotations import datetime import uuid from typing import TYPE_CHECKING import structlog from app.services import history_service from app.tasks.db import task_db from app.tasks.timeout_utils import run_with_timeout from app.utils.correlation import get_correlation_id, reset_correlation_id, set_correlation_id from app.utils.runtime_state import get_effective_settings if TYPE_CHECKING: from fastapi import FastAPI from app.config import Settings log: structlog.stdlib.BoundLogger = structlog.get_logger() #: Stable APScheduler job id. JOB_ID: str = "history_sync" #: Interval in seconds between sync runs. HISTORY_SYNC_INTERVAL: int = 300 #: Backfill window when archive is empty (seconds). BACKFILL_WINDOW: int = 648000 #: Maximum seconds to allow for history sync to complete. TASK_TIMEOUT_SECONDS: int = 60 async def _run_sync_with_settings( settings: Settings, correlation_id: str | None = None, ) -> None: """Run the history sync with correlation ID context.""" if correlation_id is None: correlation_id = str(uuid.uuid4()) token = set_correlation_id(correlation_id) try: await _do_sync_with_settings(settings) finally: reset_correlation_id(token) async def _do_sync_with_settings(settings: Settings) -> None: """Inner sync logic that runs with correlation context set.""" socket_path: str = settings.fail2ban_socket async def _do_sync() -> None: try: async with task_db(settings) as db: synced = await history_service.sync_from_fail2ban_db(db, socket_path) log.info("history_sync_complete", correlation_id=get_correlation_id(), synced=synced) except Exception: log.exception("history_sync_failed", correlation_id=get_correlation_id()) await run_with_timeout("history_sync", _do_sync(), TASK_TIMEOUT_SECONDS) async def _run_sync(app: FastAPI) -> None: await _run_sync_with_settings(get_effective_settings(app)) def register(app: FastAPI) -> None: """Register the history sync periodic job. Should be called after scheduler startup, from the lifespan handler. """ settings = get_effective_settings(app) app.state.scheduler.add_job( _run_sync_with_settings, trigger="interval", seconds=HISTORY_SYNC_INTERVAL, kwargs={"settings": settings}, id=JOB_ID, replace_existing=True, next_run_time=datetime.datetime.now(tz=datetime.UTC), ) log.info("history_sync_scheduled", interval_seconds=HISTORY_SYNC_INTERVAL)