Finalise grade syncs left stuck in_progress - #7441
Open
santicomp2014 wants to merge 1 commit into
Open
Conversation
A GradingSync only reaches a terminal status when a sync_grades_complete task observes every one of its grades as complete. That relies on a single delayed message arriving after the last grade commits. If it is lost, or fires before the commit it needed to see, nothing re-checks and the sync stays in_progress forever. That is not cosmetic: a non-terminal sync blocks all further grade syncing for its assignment. create_grading_sync returns 400, and the partial unique index enforces the same rule in the database. One lost message takes out grade syncing for that assignment permanently, until someone clears it by hand. This happened in production on 2026-08-16 (CA, assignment 9253). A grade got a 403 from the institution's LMS, exhausted its retries, and set success=False inside its transaction. The completion task scheduled one second earlier ran and finished 369 ms before that transaction committed, so it saw the grade as still pending, declined to finalise, and exited successfully. The sync sat in in_progress for three days and the instructor could not sync at all. Adds sweep_stale_grading_syncs, scheduled every 15 minutes from h-periodic. It finalises anything left non-terminal past STALE_GRADING_SYNC_TIMEOUT, and marks grades still incomplete by then as failed - their task is not coming back, and without that the sync could never reach a terminal state. Dispatch happens after the transaction commits, since scheduling work from inside a transaction is the bug being cleaned up after. Also fixes the existing test for sync_grades_complete, whose final line was an assignment rather than an assertion, so it could never fail. Correcting it showed three of its five expected values were wrong: the fixture starts at "scheduled", so an unfinalised sync stays "scheduled", not "in_progress". Background: docs/grade-sync-completion-race.md Recovery runbook: playbook docs/support-howtos/unblocking-a-stuck-grade-sync.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A
GradingSynconly reaches a terminal status when async_grades_completetask observesevery one of its grades as complete. That relies on a single delayed message arriving after
the last grade commits. If it is lost — or fires before the commit it needed to see — nothing
re-checks and the sync stays
in_progressforever.That is not cosmetic. A non-terminal sync blocks all further grade syncing for its
assignment:
and
ix__grading_sync_assignment_status_uniqueenforces the same rule in the database. So onelost message takes out grade syncing for that assignment permanently, until someone clears it
by hand.
It happened
Production, 2026-08-16, CA region, assignment 9253. A grade got a
403from the institution'sBrightspace, exhausted its retries, and set
success=Falseinside its transaction:The finaliser completed 369 ms before the commit it needed to see, found the grade still
pending, declined to finalise, and exited successfully. The sync sat in
in_progressfor threedays. The instructor could not sync at all — every attempt returned 400.
Full analysis in
docs/grade-sync-completion-race.md.The fix
sweep_stale_grading_syncs, scheduled every 15 minutes fromh-periodic(hypothesis/h-periodic — needs to merge after this one, since it references this task).
It finalises anything left non-terminal past
STALE_GRADING_SYNC_TIMEOUT, and marks gradesstill incomplete by then as failed. That second part matters: without it, a lost
sync_gradetask would leave a grade
NULLforever and the sync could never reach a terminal state — thereaper would keep declining, exactly like the original finaliser did.
Dispatch happens after the transaction commits, since scheduling work from inside a
transaction is the bug being cleaned up after.
15 minutes is a wide margin: a sync normally completes in ~1 s, and the worst legitimate case
is a grade exhausting
max_retries=2withretry_backoff=10, well under a minute.The test that should have caught this
The existing test for
sync_grades_completecould never fail — its last line was an assignment,not an assertion:
Correcting it revealed three of its five expected values were also wrong: the fixture starts at
scheduled, so a sync that is not finalised staysscheduled, notin_progress.Not included
This guarantees recovery; it does not reduce how often the race fires. Two follow-ups are
worth doing separately, and are written up in the doc:
sync_grades_completea retry policy — it is a bare@app.task(), unlikesync_grade.Testing
pg_config, sopsycopg2will not build in the tox env. Both files pass
py_compileand the logic has been reviewed, butplease let CI be the judge, and give the four new tests a careful read.
The production instance of this was cleared separately by re-running
sync_grades_completeforthat sync; recovery steps are in the playbook runbook referenced above.
🤖 Generated with Claude Code