Repository navigation
applier: wait for a delivered commit until the source shows it visible - #241
Conversation
The walsender sends a commit once its WAL is flushed, before the backend has made it visible to snapshots; under synchronous commit the gap lasts as long as the standby wait. A chunk or completion read taken in that gap does not see the change, so the drain now judges each entry's xid against pg_current_snapshot() and keeps an invisible one buffered as in flight.
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 at head Verdict: no actionable correctness defect found in the reviewed delta; verification is limited below. Reading the source snapshot before the copier's position preserves the ordering needed to discard an uncut key, while an invisible transaction now stays buffered and continues to bound confirmation. The swap uses the explicit all-visible mode only at its documented writer-excluding lock boundary. FindingsNo new findings. The decoder stamps events from BEGIN; inserts, overlays, move halves, deletes, and replacement delete markers retain the latest transaction ID. The drain treats invisible entries as in flight before pair deferral, and the zero snapshot keeps entries buffered. Snapshot tests cover both horizons, in-progress transactions, epoch boundaries, and malformed input. The blocked synchronous-commit integration test asserts buffering, zero application, and confirmation behind delivery before cancellation, then convergence afterward. Existing tests retain their assertions while explicitly opting into all-visible mode. Verified and limitationsRead the description and complete PR diff, including snapshot parsing/widening, transaction propagation, drain pairing, confirmation-related assertions, and the swap call sites; all 16 reported checks succeeded, including build, unit tests, PostgreSQL 14–18 suites, Supabase compatibility, and the built-artifact smoke test. No internal links or details found in the reviewed title/body/diff or this comment. No local build or tests were run. Direct GitHub API reads failed, so current-head repository guidance, surrounding source, and inline review threads were unavailable; I could not independently verify the unchanged pair-deferral, confirmation, or lock-acquisition implementations. The primary-key/unique-value collision described as remaining open is an existing limitation outside this delta. This review was generated by Codex (GPT-6.1 Sol). |
|
🤖 Product review at OSS adoption: This makes synchronous-replication deployments safer without adding configuration. The SchemaBot integration: The wait stays inside the catch-up loop, preserving buffered changes and withholding confirmation rather than reporting successful application. Snapshot-read errors propagate with source-table context; malformed snapshots retain the typed invariant error. Consumers can inspect This review was generated by Codex (GPT-6.1 Sol). |
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/241 No defects were found, so nothing changes in this PR. Two caveats are real follow-ups outside its scope.
Source: block/pg-sprite#241, review comments 6391741479 and 6391742898 and review 5640877415 at head |
…ename-index-kind * origin/main: schemachange: run the swap transaction at READ COMMITTED (#252) applier: wait for a delivered commit until the source shows it visible (#241) decode: refuse a relation that is not the target's OID (#225) schemachange: refuse a re-added column the copy cannot fill (#235) migrate: expose copy-and-swap progress and copy sizing on the seam (#234) copier: prove a source column rename fails the copy loudly (#232) cli: make the forced statement-budget refusal test independent of read speed (#242) migrate: refuse statically inadmissible steps in dry run (#208) executor: queue the commit-refusal test's fault only for its own table (#240) migrate: refuse a copy-and-swap connection on another database (#229) migrate: drive the live copy-and-swap path under load from the front door (#237) schemachange: keep the publication when a reconcile replaces the slot (#236) schemachange: qualify statistics, backend and fence reads with pg_catalog (#214) schemadiff: qualify every introspection catalog read with pg_catalog (#213) schemachange: let the mid-copy resume test tolerate a chunk the stopped copy landed (#215) checkpoint: report a row in an unknown phase as incompatible, not as an invariant violation (#216) schemachange: qualify the identity and sequence handoff reads (#207)
The catch-up keeps a delivered change buffered until the source's own snapshot shows its transaction committed, so a change the walsender sent before the committing backend made it visible — a synchronous-commit wait — is never discarded or completed from a read that cannot see it.
Why
PostgreSQL writes a transaction's commit record to the WAL, then waits for any synchronous standby, and only then marks the transaction visible to new snapshots. The walsender decodes the commit as soon as it is flushed, so under
synchronous_commit = on(orremote_write/remote_apply) with a slow or absent standby, the stream delivers a change whose transaction no snapshot on the source shows yet — for as long as the wait lasts. Until now CO-4 recorded this as the one Open window in the copy/apply ordering: a drain in that window could discard an uncut key's change (the copier's later chunk read would not see it either, if it ran before visibility) or complete a moved image from a source row that does not yet show the move. The engine already judges an in-flight chunk by holding its changes back; the same rule closes this window once the drain knows which delivered transactions the server has made visible.What
pkg/decode: everyChangeEventcarriesXid, the transaction's id from its BEGIN.pkg/applier/snapshot.go:Snapshot(xmin,xmax, in-progress xids, parsed frompg_current_snapshot()),ReadSnapshoton the pool,Visible(xid)widening the stream's 32-bit xid to the 64-bit id nearestxmax,AllVisiblefor a drain under a lock every writer has already released; the zeroSnapshotshows nothing visible.Buffer: eachEntrycarries the xid of the latest event merged into it (a key move stamps both halves, a delete its marker, the flush's replacement marker the image it replaced).Drain(pos, snap)treats an entry whose xid the snapshot does not show committed as in flight — kept buffered, counted inBatch.Invisible/Status.Invisible, its pair deferred with it under the existing three-way rule.Catchup: reads the snapshot on the pool before thePositionread, and only when the buffer holds anything; visibility is monotone, so a change held this cycle is judged again next cycle.pkg/schemachange/runner_drain.goflushAll: drains underAllVisible— the swap'sACCESS EXCLUSIVElock cannot be granted while a writer still holds its locks, and a writer holds them until its commit is visible.Snapshotparsing andVisibleatxmin/xmax/in-progress/wraparound (unit);Drainholding an invisible entry and its pair (unit);TestCatchupWaitsForACommitTheServerHasNotMadeVisible(integration, PG 14–18): a server withsynchronous_standby_namesnaming a standby that never connects, a writer undersynchronous_commit = onwhose UPDATE is delivered and stays buffered withInvisible == 1, nothing applied, the slot unconfirmed past it;pg_cancel_backendends the wait and the change lands and converges.docs/invariants.mdmoves the window from Open to Enforced and adds the test obligation;docs/copy-and-swap-design.mdpackage map;docs/schemabot-integration.mdno longer calls the window open.Not in this PR: the primary-key move / unique-value collision CO-4 still lists as open belongs to #239.
Before / after
Table
orders(id bigint PRIMARY KEY, amount integer), every key already landed in the shadow. A writer session withsynchronous_commit = onrunsUPDATE orders SET amount = 99 WHERE id = 2while the configured standby never acknowledges, so the backend sits in its commit wait. The WAL is flushed, so the walsender delivers the UPDATE; no snapshot on the source shows the transaction yet.Under the swap's lock the same drain runs with
AllVisible: the lock was granted only after every writer released its locks, which happens after its commit is visible, so there is nothing left to wait for.🤖 Generated with Amp (Claude Opus 4.6)