Skip to content

fix: absorb concurrent-writer insert races on unique-key models#187

Merged
bryangingechen merged 1 commit into
masterfrom
fix/concurrent-unique-key-races
Jul 2, 2026
Merged

fix: absorb concurrent-writer insert races on unique-key models#187
bryangingechen merged 1 commit into
masterfrom
fix/concurrent-unique-key-races

Conversation

@bryangingechen

@bryangingechen bryangingechen commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

A deployed analyzer.rebuild_queue_windows_sweep run failed with a duplicate-key IntegrityError on prqwin_pr_ruleset_from_unique. Root cause: the sweep and analyzer.process_pr can rebuild the same PR's queue windows concurrently (the sweep preferentially picks freshly-updated PRs which are exactly the ones process_pr is handling after a sync). Both snapshot the existing rows before either inserts, both compute the same new window, and the loser's bulk_create hits the unique constraint. Worse, the uncaught exception aborted the entire sweep run, not just that PR.

An audit of every write site against a unique-key model found the same check-then-create race in seven more places:

  • queue-window build state (same two writers),
  • PR dependency edges (per-PR task vs sweep),
  • label defs (concurrent PR syncs vs the hourly catalog task, on the case-insensitive name constraint),
  • user upserts (any two syncs ingesting the same actor),
  • PullRequest creation itself (live sync vs archive importer vs admin enqueues),
  • Zulip registration racing the syncer,
  • and the reviewer opt-out backfill racing live syncs.

All now converge on the winner's row instead of raising, using the pattern that fits each site:

  • bulk_create(update_conflicts=...) upserts for the snapshot-diff rebuilds (now also wrapped in transaction.atomic() so readers never see a half-rebuilt window set),
  • get_or_create keyed exactly on the unique constraint (__iexact lookups for the Lower() functional constraints),
  • and savepoint + IntegrityError re-fetch where two constraints are in play.

The sweep additionally contains per-PR IntegrityError (logged and reported as prs_conflict_skipped in the task result) so a residual conflict skips one PR (which stays stale and is retried next sweep) instead of poisoning the run.

Every fix has a dedicated race-path regression test that simulates losing the insert race against a pre-committed winner row (stale snapshot reads, or a forced get-miss so get_or_create's create genuinely conflicts and Django's conflict-retry get must recover). The get_or_create tests deliberately conflict across casings where the constraint is on Lower(...), pinning that the lookup kwargs cover the constraint — the property the retry relies on and the easiest one to silently break in a refactor. qb_site/AGENTS.md gains a "Concurrent Writers and Unique Keys" section codifying the rule and the approved patterns with in-repo reference files; the analyzer and syncer AGENTS.md files point to it.

Performance impact is negligible: Postgres already probes the unique index on every insert (ON CONFLICT just tells it what to do on a hit), and the new savepoints only fire on the entity-creation paths. Rollout needs no flags, migrations, or backfills; the previously failing sweep self-heals on its next run.

🤖 Generated with Claude Code

analyzer.rebuild_queue_windows_sweep crashed in production with a duplicate-key
IntegrityError on prqwin_pr_ruleset_from_unique: the sweep and analyzer.process_pr
rebuilt the same PR's queue windows concurrently, both snapshotted the existing
rows before either inserted, and the loser's bulk_create hit the unique
constraint — aborting the entire sweep run.

An audit found the same check-then-create race at seven other write sites.
All now converge on the winner's row instead of raising:

- queue_windows: bulk_create upsert (update_conflicts) keyed on
  (pull_request, rule_set, from_ts); create/update/delete wrapped in
  transaction.atomic so readers never see a half-rebuilt window set.
- queue_window_build_state: same upsert on (pull_request, rule_set).
- rebuild_queue_windows_sweep: contain per-PR IntegrityError (logged and
  reported as prs_conflict_skipped) so one conflict cannot poison the run;
  the skipped PR stays stale and is retried next sweep.
- dependencies: get_or_create on the unique edge triple (per-PR task vs sweep).
- labels_sync (both functions): get_or_create with name__iexact lookup so the
  conflict-retry get covers the (repository, lower(name)) constraint.
- core_entities_sync.upsert_user_from_github: savepoint + IntegrityError
  re-resolve on the case-insensitive login constraint (any two concurrent
  PR syncs ingesting the same actor).
- pull_request_sync.upsert_pull_request: savepoint + re-fetch on
  (repository, number) (live sync vs archive importer vs admin enqueues).
- registration_linking: savepoint + re-resolve, falling through to the
  linking path when the syncer creates the same GitHub user concurrently.
- reviewer_opt_out_backfill: get_or_create on
  (repository, pr_number, reviewer_login).

Every fix has a dedicated race-path regression test that simulates losing
the insert race (stale snapshot or forced get-miss against a pre-committed
winner row). The get_or_create tests specifically pin that the lookup kwargs
cover the unique constraint — the property Django's conflict-retry get relies
on, and the easiest one to break in a future refactor.

qb_site/AGENTS.md gains a "Concurrent Writers and Unique Keys" section
codifying the approved patterns (with in-repo reference files), and the
analyzer/syncer AGENTS.md files point to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryangingechen bryangingechen merged commit d5628e8 into master Jul 2, 2026
4 checks passed
@bryangingechen bryangingechen deleted the fix/concurrent-unique-key-races branch July 2, 2026 13:37
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.

1 participant