Skip to content

Retry home and commitfest views on serialization failures - #125

Merged
JelteF merged 3 commits into
mainfrom
retry-serialization-failures
Oct 18, 2026
Merged

JelteF merged 3 commits into
mainfrom
retry-serialization-failures

Conversation

@JelteF

@JelteF JelteF commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

When two requests to the home or commitfest views race during a commitfest rollover, the lazy commitfest refresh makes one of them fail with "could not serialize access due to concurrent update", because these views run at REPEATABLE READ. That resulted in a 500 for the user. Now these views are retried once in a fresh transaction on serialization failures and deadlocks. At that point the other request has already committed the refresh, so there's nothing left to conflict with.

The `home` and `commitfest` views run at REPEATABLE READ so all their queries
see one snapshot. Rendering them can also trigger the lazy commitfest refresh
in `CommitFest.relevant_commitfests()`, which locks rows with
`SELECT ... FOR UPDATE`. When two requests race during a commitfest rollover,
the one that loses can't lock the row that the other just updated, so
PostgreSQL aborts it with "could not serialize access due to concurrent
update" and the user gets a 500.

These views are now wrapped in `retry_on_serialization_failure`, which sits
outside `transaction.atomic` and reruns the view in a fresh transaction when
it fails with a serialization failure (40001) or deadlock (40P01). On retry
the winning request has already committed the refresh, so the up-to-date check
short-circuits and nothing conflicts anymore. Attempts are capped at five so a
persistent conflict still surfaces as an error.
@JelteF
JelteF marked this pull request as ready for review October 18, 2026 07:40
The four retry tests shared the same body and only differed in the
SQLSTATE raised, how often it was raised, and the expected number of
attempts, so they are now a single parametrized test. The non-retryable
case uses 57014 (query_canceled) instead of 23505, because a unique
violation surfaces as `IntegrityError` rather than `OperationalError` in
Django, which made that case unrealistic.
The refresh only writes once per rollover, so once the request that won
the race has committed, a retry has nothing left to conflict with. If
that single retry still fails, something else is wrong and it's better
to surface the error than to keep rerunning the whole view.
@JelteF
JelteF merged commit 5e72c55 into main Oct 18, 2026
2 checks passed
@JelteF
JelteF deleted the retry-serialization-failures branch October 18, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants