Review
Nightly promoter settlement invoices via in-process APScheduler
Fathomfee settles small-venue shows after doors close. Ops wants promoter invoices (PDF + Stripe draft) out by 02:15 UTC without keeping Celery Beat solely for one job. Register a BackgroundScheduler CronTrigger inside the API process so every deploy that can serve traffic also owns the nightly settlement send.
APSchedulerTier 5crondistributed-lockreplicasinvoicing
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
fathomfee/jobs/promoter_settlements.py+19-6
| 1 | 1 | """Nightly promoter settlement invoices for last-night's shows.""" | |
| 2 | 2 | ||
| 3 | 3 | from apscheduler.schedulers.background import BackgroundScheduler | |
| 4 | 4 | from apscheduler.triggers.cron import CronTrigger | |
| 5 | 5 | ||
| 6 | 6 | from fathomfee.billing import create_and_email_settlement_invoice | |
| 7 | 7 | from fathomfee.shows import closed_shows_for_night | |
| 8 | 8 | ||
| 9 | + | # Run inside each API task so we do not need a dedicated Beat worker for one cron. | |
| 10 | + | scheduler = BackgroundScheduler(timezone="UTC") | |
| 11 | + | ||
| 9 | 12 | def send_promoter_settlements() -> None: | |
| 10 | 13 | for show in closed_shows_for_night(): | |
| 11 | 14 | create_and_email_settlement_invoice(show.id, show.promoter_id) | |
| 12 | 15 | ||
| 13 | - | # Invoked only by the single Celery Beat process (see deploy/celery-beat). | |
| 14 | - | def register_beat_task(app) -> None: | |
| 15 | - | app.conf.beat_schedule["promoter-settlements"] = { | |
| 16 | - | "task": "fathomfee.jobs.promoter_settlements.send_promoter_settlements", | |
| 17 | - | "schedule": crontab(hour=2, minute=15), | |
| 18 | - | } | |
| 16 | + | def register_jobs() -> None: | |
| 17 | + | # max_instances prevents overlap if a send runs long on *this* process. | |
| 18 | + | scheduler.add_job( | |
| 19 | + | send_promoter_settlements, | |
| 20 | + | CronTrigger(hour=2, minute=15), | |
| 21 | + | id="promoter-settlements-nightly", | |
| 22 | + | replace_existing=True, | |
| 23 | + | max_instances=1, | |
| 24 | + | coalesce=True, | |
| 25 | + | ) | |
| 26 | + | scheduler.start() | |
| 19 | 27 | ||
| 28 | + | ||
| 29 | + | # Called from FastAPI lifespan on every replica that boots. | |
| 30 | + | def on_app_startup() -> None: | |
| 31 | + | register_jobs() | |
| 32 | + | ||
| 20 | 33 | # create_and_email_settlement_invoice always creates a new Stripe invoice + PDF. |