68 lines
3 KiB
Python
68 lines
3 KiB
Python
"""Recurring worker-tier jobs: keep the curated city set warm and ping IndexNow
|
|
on a schedule, so neither needs a human to run it after every deploy anymore.
|
|
|
|
Only the process that already won leader election for the notifier runs this
|
|
too (gated by the same check in web/app.py's _lifespan) — APScheduler has no
|
|
cross-host coordination of its own, so running it unguarded on every worker
|
|
replica would repeat every job once per replica, same reason the notifier
|
|
itself needs a leader.
|
|
|
|
APScheduler, not a Postgres-native queue: there is exactly one process that
|
|
ever runs this, no fan-out/backpressure/dead-letter need, and it needs no new
|
|
infrastructure. procrastinate/pgqueuer are the documented upgrade path if that
|
|
changes — see docs/architecture/repo-topology-and-infrastructure.md §7.
|
|
"""
|
|
import os
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
# How often the curated city set is (re-)warmed. warm_cities.py's own docstring
|
|
# frames it as a deploy-time script ("Run at/after deploy") because a warm cache
|
|
# mostly just stays warm; scheduling it periodically instead catches a newly
|
|
# added city, or one that fell out of cache, without waiting for the next
|
|
# deploy. Daily is plenty — every already-cached city is a cheap skip.
|
|
WARM_CITIES_INTERVAL_HOURS = int(os.environ.get("THERMOGRAPH_WARM_CITIES_INTERVAL_HOURS", "24") or 24)
|
|
|
|
# How often to check whether the indexable URL set changed and, if so, ping
|
|
# IndexNow. submit_if_changed() is a cheap no-op when nothing changed, so this
|
|
# can run more often than warm_cities without spending anything on most ticks.
|
|
INDEXNOW_INTERVAL_HOURS = int(os.environ.get("THERMOGRAPH_INDEXNOW_INTERVAL_HOURS", "6") or 6)
|
|
|
|
_scheduler: "BackgroundScheduler | None" = None
|
|
|
|
|
|
def _warm_cities_job() -> None:
|
|
import warm_cities # local import: a script-style top-level module — only
|
|
# pay its import cost on the process that runs the job
|
|
warm_cities.main()
|
|
|
|
|
|
def _indexnow_job() -> None:
|
|
import indexnow
|
|
indexnow.submit_if_changed()
|
|
|
|
|
|
def start() -> None:
|
|
"""Start the scheduler (idempotent, mirroring notify.start()). The caller is
|
|
responsible for the leader-election gate — see web/app.py's _lifespan."""
|
|
global _scheduler
|
|
if _scheduler is not None:
|
|
return
|
|
sched = BackgroundScheduler(daemon=True)
|
|
# No next_run_time override: the default first run is one interval from now,
|
|
# not immediately — warm_cities already runs at deploy time (warm_cities.py /
|
|
# the deploy hook), so an immediate duplicate run on every worker boot/restart
|
|
# would just be wasted work.
|
|
sched.add_job(_warm_cities_job, "interval", hours=WARM_CITIES_INTERVAL_HOURS,
|
|
id="warm_cities")
|
|
sched.add_job(_indexnow_job, "interval", hours=INDEXNOW_INTERVAL_HOURS,
|
|
id="indexnow")
|
|
sched.start()
|
|
_scheduler = sched
|
|
|
|
|
|
def stop() -> None:
|
|
global _scheduler
|
|
if _scheduler is not None:
|
|
_scheduler.shutdown(wait=False)
|
|
_scheduler = None
|