Repository navigation
copier: retry a chunk whose insert collides with a unique index - #239
Conversation
A unique value the source moves between a landed key and a key in flight collides on the chunk's insert until the applier has flushed the move; the copy used to stop on the first 23505. Each attempt re-reads the source, so the chunk lands once the flush commits. The catch-up convergence test for unique-key swaps now runs its load throughout the copy.
Resolves docs/copy-and-swap-design.md (copier package row: main's conversion-fingerprint guard kept, this branch's collision-retry tail kept) and docs/invariants.md (CO-4: main's sync-commit xid gate now Enforced, followed by this branch's copier-side collision retry and the remaining PK-move-with-unique-secondary gap).
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial correctness review, performed by Jash's AI agent. Reviewed at head Verdict: no correctness blocker identified in the diff; one optional coverage improvement. The retry remains scoped to the two collision SQLSTATEs, preserves the server error on exhaustion, and lands a chunk only after success. The documented primary-key-move limitation remains explicit. Findings1. (optional coverage) The new load test can pass without a collision. Action items
Verified (tried to break, couldn't)Reviewed the full eight-file diff against the exact live head/file metadata; checked SQLSTATE classification, attempt count, context-aware wait, error wrapping, position/row-count updates and successful-attempt timing; no test removals or weakened assertions; GitHub reports all 16 checks successful. Exact-head surrounding source and AGENTS.md were unavailable in this environment, so no local build/tests or helper-source verification were performed. This review was generated by Codex. |
|
🤖 Two-lens review at OSS adoption ease: Automatic bounded retries improve the default experience for unique-value moves. The final error retains the database detail plus chunk bounds and attempt count; the remaining primary-key-move limitation is documented. No adoption blocker found in the diff. SchemaBot integration: This review was generated by Codex. |
JashLal
left a comment
There was a problem hiding this comment.
🤖 Approving on Jash's behalf after the review at 8d35e2e. No concrete correctness blocker found in the full diff; the optional deterministic inter-component collision test is recorded in the review. Exact-head unchanged helpers and repository instructions were inaccessible, so no local build or tests ran. Approval does not certify those unverified paths. Generated by Codex.
The load-driven test can pass without the race happening. Holding the catch-up's flushes while a unique value moves off a landed key makes the copier's chunk collide on every run, then checks the retry lands and the shadow converges.
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/239, follow-up commit The one finding is fixed: a new convergence test forces the copier/applier collision on every run instead of leaving it to the load's timing.
Source: block/pg-sprite#239, review comments 6454179618 and 6454181892 and review 5674719763 at head |
Resolves the package-map conflict in docs/copy-and-swap-design.md: this branch's pkg/copier row and main's pkg/checksum row (recopy collision retry) are both kept.
Resolves the three conflicts #239 landing on main left: the design doc's pkg/checksum row takes main's recopy-collision text (this branch never changed it), and invariants.md plus the key-move test comment keep this branch's wording, which supersedes the CO-4 *Open* paragraph #239 wrote for the landed-to-in-flight PK move.
The copier runs a chunk again, after a bounded wait, when its insert collides with a shadow row on a unique index or exclusion constraint, instead of stopping the copy on the first
23505.Why
The shadow is built
LIKE … INCLUDING ALL, so every unique index on the source exists on the shadow while the copy runs. The copy and the catch-up write the shadow at the same time: a source transaction that moves a unique value from a landed row to a row in a chunk still in flight leaves the shadow holding the value under the landed key until the applier flushes that change. If the chunk's insert reaches the shadow first, the server refuses the whole chunk with23505, and until now the copy ended there — a correct, converging race ended the schema change, and the convergence tests for unique-key swaps had to start their load only after the copy had landed every key.The resolution is the copier's, not the catch-up's: each chunk attempt is its own transaction with its own snapshot, so once the flush has committed, a fresh attempt reads the moved rows as the source now holds them and lands.
What
pkg/copier/retry.go:copyUntilClearrunscopyChunkuntil it commits; on23505/23P01(matched by SQLSTATE) it counts the collision, waitsCollisionBackoff × attempt(context-aware), and tries again; afterCollisionAttemptscollisions it returns the server's*pgconn.PgErrorwrapped with the chunk bounds and the attempt count, chunk left in flight. Any other error is the first attempt's error, unretried.Collisions()exposes the count.Options.CollisionAttempts/Options.CollisionBackoff(defaults 10 and 250 ms;validaterefuses attempts below one and a negative backoff).TestCatchupConvergesUnderUniqueKeyMovesDuringTheCopyinpkg/applier, unique-key swaps running from before the first chunk is claimed until after the copy, converging; andTestCatchupConvergesWhenTheCopierCollidesWithAnUnflushedMove, which forces the copier/applier race on every run: with the catch-up's flushes paused, a source transaction moves a unique value from a landed key to a key not yet copied, the copier's chunk collides, the flushes resume, the retried chunk lands, and the shadow converges.docs/invariants.mdmarks the copier's side Enforced and narrows the Open text to the one case this does not resolve (below);docs/copy-and-swap-design.mdpackage map andpkg/copier/doc.go.Not in this PR: a primary-key move whose row also carries a unique secondary value, from a landed key to a key in flight. The drain's pair rule defers the old key's delete until the chunk lands, and the chunk cannot land while the old key's row holds the value, so the two wait on each other until the attempts run out.
TestCatchupConvergesUnderKeyMoveskeeps its load after the copy and says why; this is tracked as an internal follow-up.Before / after
Table
seats(id bigint PRIMARY KEY, slot integer UNIQUE). The copier is reading chunk[1, 100]; key 2000 has already landed withslot = 15. A source transaction moves slot 15 from row 2000 to row 42 (and gives 2000 a fresh slot), committing while the chunk is in flight. Row 2000's half of the change is for a landed key, so the applier can flush it; row 42's half waits for its chunk.🤖 Generated with Amp (Claude Opus 4.6)