All scheduling now runs through a dispatcher #421
No reviewers
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web!421
Loading…
Reference in a new issue
No description provided.
Delete branch "phase-4-rewire-the-scheduler-onto-the-week-table-410"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Every job used to be registered at startup and left alone, so cadence was
fixed at boot and nothing re-derived it. Now lifespan registers exactly one
job -- the dispatcher -- which runs every minute, reads state and converges
the schedule toward what that state calls for.
Registration is compare-then-replace rather than unconditional re-add:
add_job(replace_existing=True) recomputes next_run_time as now + interval, so
a per-minute dispatcher would starve an hourly job forever.
Notable pieces:
while a game is inside its kickoff window. It costs 1 API call against the
full sync's 5, because it reads getNFLScoresOnly directly instead of
building NflApi.games. A flag on the existing job could not have saved
anything -- by the time it applies, the calls are already made.
getNFLScoresOnly, with fixtures where the two endpoints disagree.
and last success per job. The admin schedule page shows a light per job,
red when nothing has succeeded within two of its own periods.
setting-up page, and admin pages stay reachable so the sync can be kicked.
Deferred: award scheduling (#420).
Resolves #410
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
This is a large, well-structured PR. I've reviewed the dispatcher, the new/changed jobs, the API changes, the routing/cold-start handling, and checked for dangling references to the deleted modules. No blocking defects; a few things worth confirming.
Solid refactor — the compare-then-replace convergence logic is sound, the cold-start 503 path is well thought through, and the status-from-scores fix is correctly threaded through both
NflApiGameand the score sync with fixtures that encode the real disagreement. Below are a handful of things worth a second look, mostly around behavior changes and test coverage rather than outright bugs.Worth fixing
Awards are no longer recalculated on any schedule, and picks submission no longer triggers a recalc.
app/main.py:357drops theadd_jobthat used to enqueueupdate_all_awardsafter a player submits picks, andschedule_award_updatesis gone from the scheduler with no replacement inapp/jobs/dispatcher.py:211. The description notes award scheduling is deferred to #420, but the net effect is thatPlayerAwardrecords will go stale for the whole season until #420 lands — worth confirming that's the intended interim state and not an oversight, since nothing in the UI will hint that awards have stopped updating.update_player_recordsonly runs on a final transition caught inside the live window.app/jobs/sync_scores_for_current_week.pyrecomputes player records only whennewly_finalflips, and the score-sync job only exists while a game is within[kickoff-5min, kickoff+5h](_in_live_window,app/jobs/dispatcher.py:24). The oldupdate_a_gamejob polled for 8h and the hourly full sync (sync_games_for_current_week) does not callupdate_player_recordsat all. A game that finalizes more than 5h after kickoff (or a final the window otherwise misses) leaves player records stale until the next final anywhere triggers a full-season rebuild. Practically rare, but there's now no fallback reconciliation path.Test coverage gap on the core of the PR. The added tests (
tests/test_tank01_api.py::TestStatusComesFromTheScoresEndpoint) only cover the status-source change. There are no tests for the pieces this PR is actually about: the convergence/compare-then-replace logic and_in_live_windowinapp/jobs/dispatcher.py,sync_scores_for_current_week'snewly_finalgating,lock_odds(including theOddsLockExceptionpath), or the_record_job_runlistener /JobStatehealth rendering._in_live_window,_job_health, and the score add/remove branching are all pure-ish and cheap to unit-test.Nits
Inconsistent
NflApiconstruction costs an extra API call.sync_games_for_current_weekbuildsNflApi(week_no=..., season_type=...)(app/jobs/sync_games_for_current_week.py:84) withoutseason, socurrent_infocan't short-circuit and spends a/getNFLCurrentInforound trip — whereassync_scores_for_current_weekpasses all three overrides and skips it. Passingseason=current_week.seasonhere would make it consistent and one call cheaper.JobStaterows for the nag jobs accumulate unbounded. Nag job ids are per-week (s{season}:st:{type}:w{week}:d{delta},app/jobs/dispatcher.py:197), so_record_job_runwrites a freshjobstaterow every week that's never cleaned up. Harmless in volume, but the schedule page will slowly fill with dead one-shot rows._schedule_score_sync_games_for_current_weekomitsnext_run_timewhere every sibling_schedule_*passesnext_run_time=now(app/jobs/dispatcher.py:107). It's harmless (first run lands at now+1min, and the guard prevents re-adds), but the asymmetry reads like an oversight — a one-line comment or the explicitnext_run_timewould settle it.sync_week_state_from_game_statuslogs the missing-week case aterror(app/jobs/sync_week_state_from_game_status.py:42) while every other job treats the same cold-start condition asinfo/warning. On a fresh DB this will page as an error event through the Sentry logging integration until the first sync lands.