Restore award recalculation #423
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!423
Loading…
Reference in a new issue
No description provided.
Delete branch "fix-awards-never-recalculated-422"
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?
update_all_awards()lost every caller when scheduling moved to thedispatcher, so no award has been recalculated and no award notification has
reached Discord since that release. Production surfaced it on restart, as
APScheduler dropping the last stale jobstore row:
Nothing was lost — awards are recomputed from picks and games on every run, so
they reappear on the first pass after this ships.
Changes
award_update_all.py—update_all_awards()takes aSessioninsteadof opening one, matching
update_player_records.scheduled_update_all_awards()is the zero-arg entry point the schedulerneeds.
sync_scores_for_current_week.py— calls it on thenewly_finaltransition, after
update_player_records. This is the primary path: a gamegoing final is the only mid-week event that can earn an award, and
send_award_notification()fires from the tail of the same call.dispatcher.py—_schedule_update_all_awards(), Tue 5:30am PT, guardedlike
_schedule_sync_team_records. Backstop for slates that finish while theapp is down.
test_integrity.py— everyadd_job("module:function", ...)string underapp/must name a module that exists and defines that function. APSchedulerresolves these at fire time, so a rename fails in production rather than CI.
Testing
145 passed, pylint 10.00/10, flake8 clean. The new check was verified by
pointing the award job at a nonexistent function and confirming it fails.
Version bumped 2.18.0 → 2.18.1.
resolves #422
🤖 Generated with Claude Code
This is a well-constructed PR. Let me verify one last thing about how the awards interact during the newly-final path versus week-state completion, then write the review.
The design is coherent. Here's my review:
Summary
This restores the award-recalculation path that lost all callers when scheduling moved to the dispatcher, wiring it into both the per-game "newly final" transition and a Tuesday backstop cron, and adds an AST integrity test that catches dangling
add_job("module:function")references in CI instead of at APScheduler fire time. The change is correct, well-scoped, and the new week-state guards on the week-aggregate awards are exactly right; I found no blocking issues.The two structural decisions that matter both check out:
sync_perfect_week,sync_won_the_week) now early-return unlessweek.game_state == WeekState.ALL_FINAL, so the Thursday-night "everyone is 1-0" false perfect-week is prevented (app/jobs/award_update_all.py:25,:71). The per-game (in_your_face) and first-submitter (quick_pick) awards correctly remain ungated since they can be earned mid-week.IN_PROGRESSweek and skips the aggregate awards, whilesync_week_state_from_game_status(5-min cadence) flips the state toALL_FINALand re-runs the sync — so the aggregate awards land within ~5 minutes and duplicate work is idempotent via thePlayerAwardunique constraint andnotified_atdedup. Coverage is complete.Worth fixing
_sync_week_awards(app/jobs/award_update_all.py:105),sync_perfect_weekruns first and raisesAwardSyncException("Too few players")when fewer than 2 active players exist. The surroundingtry/exceptcatches it and skips the remaining three syncs — includingsync_quick_pickandsync_in_your_face, which have no minimum-player requirement. In a real pool this never fires, but the ordering couples unrelated awards to a check that only two of the four care about. Consider making each sync self-contained (catch per-sync, or move the "too few players" checks so they only skip their own award).Nits
sync_scores_for_current_week(1-min) andsync_week_state_from_game_status(5-min) are distinct APScheduler jobs and can overlap.upsert_award_with_argsdoes a select-then-insert and commits per-award, so a genuinely concurrent grant of the same award would hit theuq_playeraward_player_award_weekconstraint and error the losing job. It's self-healing (next run recomputes) and low-probability, but it'll show up as Sentry noise once the season is live.tests/test_award_sync.pynicely covers the guard logic in_sync_week_awards, but nothing exercises theweek_completedtransition detection insync_week_state_from_game_status.py:47-54(the!= ALL_FINAL → == ALL_FINALedge, and that re-running when alreadyALL_FINALdoes not re-fire). A small test there would lock in the "fires exactly once per completion" contract.sqlite_whereto the partial lock index (app/models/player_game_pick.py:79) is what lets the new tests insert multiple non-lock picks per player/week under SQLite; without it SQLAlchemy would emit a full unique index on that dialect. Worth calling out in the PR description since it's a correctness fix for the test DB, not just cosmetic. Production (Postgres) is unaffected, so no migration is needed — correct.The integrity test (
tests/test_integrity.py) is a nice touch and directly addresses the root cause of #422; the "guard the guard"test_job_references_existis the right instinct so the scan can't pass vacuously.