"""Cron entrypoint. Runs as a FastAPI app (for /health/cron) with APScheduler spawning the time-triggered jobs. P1 jobs: - poll_live_positions : every TRACKSOLID_POLL_INTERVAL_SEC (default 60s) - poll_stale_imeis : every TRACKSOLID_STALE_POLL_INTERVAL_SEC (default 600s) SLO measurement worker (#12) and contract checker (#13) land here later. """ from collections.abc import AsyncIterator from contextlib import asynccontextmanager import structlog from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger from fastapi import FastAPI from app.config import get_settings from app.db import close_pool, get_pool from app.health import router as health_router from app.logging_setup import configure_logging from app.tracksolid.client import TracksolidClient from app.workers import poller log = structlog.get_logger("cron") @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: configure_logging() settings = get_settings() await get_pool() log.info( "cron.starting", git_sha=settings.app_git_sha, mode=settings.app_mode, target_account=settings.tracksolid_target_account or "", ) client = TracksolidClient(settings) scheduler = AsyncIOScheduler(timezone="UTC") async def _run_list() -> None: await poller.poll_live_positions(client, settings) async def _run_stale() -> None: await poller.poll_stale_imeis(client, settings) if settings.tracksolid_target_account and settings.tracksolid_app_key: scheduler.add_job( _run_list, trigger=IntervalTrigger(seconds=settings.tracksolid_poll_interval_sec), id="poll_live_positions", max_instances=1, coalesce=True, misfire_grace_time=30, ) scheduler.add_job( _run_stale, trigger=IntervalTrigger(seconds=settings.tracksolid_stale_poll_interval_sec), id="poll_stale_imeis", max_instances=1, coalesce=True, misfire_grace_time=120, ) scheduler.add_job( _run_list, trigger="date", # fire once on startup id="poll_live_positions_initial", ) log.info( "cron.tracksolid_jobs_registered", list_every_sec=settings.tracksolid_poll_interval_sec, stale_every_sec=settings.tracksolid_stale_poll_interval_sec, stale_after_sec=settings.tracksolid_stale_after_sec, ) else: log.warning("cron.tracksolid_jobs_skipped_missing_creds") scheduler.start() log.info("cron.scheduler_started") try: yield finally: scheduler.shutdown(wait=False) await client.close() await close_pool() app = FastAPI( title="fleet-platform [cron]", version=get_settings().app_git_sha, lifespan=lifespan, ) app.include_router(health_router)