127 lines
4 KiB
Python
127 lines
4 KiB
Python
"""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 geocoder, poller, slo_metrics
|
|
|
|
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 "<unset>",
|
|
)
|
|
|
|
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)
|
|
|
|
has_target = bool(settings.tracksolid_target_account or settings.tracksolid_targets)
|
|
if has_target 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")
|
|
|
|
# Reverse-geocoder (PRD F2.3, pulled forward so popup shows addresses).
|
|
async def _run_geocode() -> None:
|
|
await geocoder.geocode_pending(settings)
|
|
|
|
scheduler.add_job(
|
|
_run_geocode,
|
|
trigger=IntervalTrigger(seconds=settings.geocoder_tick_sec),
|
|
id="geocode_pending",
|
|
max_instances=1,
|
|
coalesce=True,
|
|
misfire_grace_time=120,
|
|
)
|
|
log.info("cron.geocoder_registered", tick_sec=settings.geocoder_tick_sec)
|
|
|
|
# SLO measurement worker — populates slo.measurements every 60s so
|
|
# slo.v_current_status (used by the dashboard SLO panel) has live data.
|
|
scheduler.add_job(
|
|
slo_metrics.record_all,
|
|
trigger=IntervalTrigger(seconds=60),
|
|
id="slo_record_all",
|
|
max_instances=1,
|
|
coalesce=True,
|
|
misfire_grace_time=30,
|
|
)
|
|
scheduler.add_job(slo_metrics.record_all, trigger="date", id="slo_initial")
|
|
log.info("cron.slo_worker_registered")
|
|
|
|
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)
|