Fix UniqueViolation in equivalent_identifiers_refresh cache rebuild#3553
Open
dbernstein wants to merge 1 commit into
Open
Conversation
The refresh task rebuilds recursiveequivalentscache with delete-then-insert per parent identifier, assuming it is the only writer. It is not: the Identifier creation listener inserts (id, id) self-references from webapp transactions, and a redelivered task (acks_late + reject_on_worker_lost, plus self-replacement across thousands of batches) can run a second refresh chain concurrently on overlapping chains. A row committed by either between our delete and our flush collides with the constraint and aborts the whole task. Route all cache inserts through a shared _insert_cache_rows helper using INSERT ... ON CONFLICT DO NOTHING. Since every parent's rows are recomputed from the same source equivalencies, a row slipped in under us holds the same value we were about to write, so skipping the conflict converges on the correct chain. Also collapse the per-parent deletes into a single sorted IN (...) statement to reduce deadlock risk between racing refreshes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Greptile SummaryThis PR makes the equivalent identifier cache refresh tolerate duplicate cache rows. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (1): Last reviewed commit: "Fix UniqueViolation in equivalent_identi..." | Re-trigger Greptile |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3553 +/- ##
==========================================
- Coverage 93.46% 93.46% -0.01%
==========================================
Files 512 512
Lines 46614 46618 +4
Branches 6352 6353 +1
==========================================
+ Hits 43570 43571 +1
- Misses 1968 1969 +1
- Partials 1076 1078 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Description
Routes all
recursiveequivalentscacheinserts in theequivalent_identifiers_refreshtask through a shared_insert_cache_rowshelper that uses PostgreSQLINSERT ... ON CONFLICT (parent_identifier_id, identifier_id) DO NOTHING, so a concurrently-committed cache row no longer aborts the task. Also collapses the per-parent deletes into a single sortedWHERE parent_identifier_id IN (...)statement and chunks/streams the inserts.Motivation and Context
Production is throwing:
The refresh task rebuilds the cache with a delete-then-insert per parent identifier, implicitly assuming it is the only writer of the table. It is not:
Identifiercreation listener inserts(id, id)self-reference rows from ordinary webapp/import transactions, independent of this task.task_acks_late=True/task_reject_on_worker_lost=Trueand re-queues itself viatask.replace()across thousands of batches (replaced_task_nesting: 5348in the failing log). A lost worker or a lapsed visibility timeout lets the broker redeliver, and a second refresh chain can run concurrently on overlapping identifier chains. The RedisTaskLockonly guards against the beat schedule starting a second run — a redelivery carries the sameroot_id, so it re-acquires the same lock and slips through.When either writer commits a conflicting row between our delete and our flush, the batched
INSERT ... RETURNINGhits the unique constraint and the whole task aborts. Since every parent's rows are recomputed from the same source equivalencies, a row slipped in under us holds the same value we were about to write, soON CONFLICT DO NOTHINGconverges on the correct chain instead of failing.How Has This Been Tested?
TestInsertCacheRowsandTestProcessIdentifierIds::test_tolerates_rows_the_delete_did_not_remove. Confirmed both reproduce the productionIntegrityErrorwhen theON CONFLICTclause is removed, and pass with it in place.test_refresh_equivalents.py,test_equivalents.py, andtest_listeners.pypasses.mypyand pre-commit checks are clean.Checklist
🤖 Generated with Claude Code