copier: fill the copy step's progress counters from the copier - #132
Conversation
3849622 to
dd72fb6
Compare
The copier is the tracker's work source for the lifetime of Run: rows from committed chunks, the source's catalog row count read once, and both tables' pg_table_size measured at each poll. Measured, not estimated.
dd72fb6 to
aac9ebe
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial review (1/2): 1 blocking, 1 non-blocking. I reviewed The handoff holds:
I made 11 deliberate breaks to the code and 8 were caught. The caught ones:
Of the three survivors, moving Blocking1.
The With PostgreSQL 16's default
The progress figure is the smaller problem. The larger one is that any role that can create a function in a schema on the copier's path gets its code run by pg-sprite during every poll of a live copy. On PostgreSQL 14, that includes The The fix is the two constants: const rowsTotalSQL = `SELECT COALESCE((SELECT reltuples FROM pg_catalog.pg_class WHERE oid OPERATOR(pg_catalog.=) $1::pg_catalog.oid), -1)`
const tableSizesSQL = `SELECT COALESCE(pg_catalog.pg_table_size($1::pg_catalog.regclass), 0), COALESCE(pg_catalog.pg_table_size($2::pg_catalog.regclass), 0)`With both constants changed, the test below passes, and so do the PR's copier tests and Test case: fails at
|
|
🤖 Adoption and integration review (2/2): 0 blocking, 2 non-blocking. OSS adoption 1.
I tested this with the fixture's pool configuration, minus
The
The first keeps the copier self-contained, as the chunk path already is. Integration ease for importers 2. On a resumed copy, This review was generated by Claude Code (claude-opus-5-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving with comments: 1 blocking finding in the review above. Work's catalog reads need pg_catalog. qualification, per CO-9 and the WorkSource contract.
This review was generated by Claude Code (claude-opus-5-5).
Every catalog name the copier's measurements read is pg_catalog-qualified (CO-9): the row-count read, the size read, and the chunk guard's relation identity check. An unqualified pg_table_size(oid) resolved by signature across the whole search_path, so a same-signature function in a user schema won regardless of path order. Each measurement now runs in a read-only transaction of its own under the chunk budgets (LK-2), so a poll on a caller-built pool without session timeouts still ends at the copier's lock timeout instead of waiting out a queued lock. setBudgets is the one place the budgets are spelled; chunk transactions and measurements share it. Tests: a shadowing search_path with an impostor pg_class row and a pg_table_size(oid) decoy leaves the counters real and the decoy uncalled; a poll during the resume clear reports the copier's counters; a failed row-count read ends Run before the clear touches the shadow; a poll queued behind an ACCESS EXCLUSIVE request on a raw pgxpool ends with the server's lock timeout (55P03), not the observer's patience. Docs: the progress report states that on a resumed run rows_copied / rows_total is that run's share, not completion, and that the size read is bounded by the copy's own budgets.
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/132, follow-up commit All four findings fixed in the follow-up commit; the
Source: block/pg-sprite#132, review comments 5921080138 and 5921080298 and review 5372845561 at head |
Resolves the pkg/checksum rows in SAFETY.md, docs/architecture.md, and docs/copy-and-swap-design.md against the copier progress-filler rows from #132: this branch's checksum text, main's copier and progress text.
Makes
copier.Copierthe progress tracker'sWorkSourcefor the lifetime ofRun, so a poll during the copy step reports rows and bytes instead of an empty step. Built on the progress contract from #131 (merged).Why
The tracker can now poll an engine-measured
WorkSource(#131), but nothing implements one. The copier already holds every fact the step's counters need — the ledger's inserted-row count, the two relation OIDs the shadow proof carries — and it is the only component that knows when the copy is running. Wiring it here, behind oneOptionsfield, keeps the orchestrator PR to orchestration.What
pkg/copier:Options.Tracker *progress.Tracker. When set,RuncallsSetWorkSource(c)before the resume clear and the first chunk, andStopWorkSource()(the drain fence) before it returns; the caller owns the tracker's steps and statement, the copier fills only the current step's counters. A nil tracker changes nothing.Copier.Work(ctx)—rows_copiedisPosition.RowsInserted(rows this run's committed chunks inserted; a resumed run counts only its own);rows_totalis the source'spg_class.reltuplesread once at the start ofRun, clamped so a never-analyzed table (-1) reports0rather than an unsigned wraparound;bytes_copied/bytes_totalarepg_table_sizeof the shadow and the source measured at each poll over the caller's pool. OutsideRunthe copier holds no connection, so the sizes read0and the row counters still report what the ledger knows.COALESCE(…, -1)), so the chunk guard's ST-6 refusal stays the one answer for a replaced or dropped source (the existingTestCopierRefusesReplacedRelationsfails without this).munow also guards the bound pool and the recorded total;report/its stop closure are the only writers.pg_catalog-qualified (CO-9):pg_class,pg_table_size, the=operator, and theoidcast — an unqualifiedpg_table_size(oid)resolves by signature across the wholesearch_path, so a same-signature decoy in a user schema wins regardless of path order. The chunk guard's relation-identity query (relationOIDsSQL) is qualified the same way.SET LOCAL lock_timeout/statement_timeout, LK-2), so a poll on a caller-built pool with no session timeouts still ends at the copier's lock timeout instead of waiting out a queued lock.setBudgetsis the one place the budgets are spelled; chunk transactions and measurements share it.progress-report.mdgains a table defining the four copy counters, notes that the two sizes are of tables that differ in shape (a rate is derived by the consumer from two snapshots), states that on a resumed runrows_copied/rows_totalis that run's share rather than completion, and that the size read is bounded by the copy's own budgets;copy-and-swap-design.mdpackage map,architecture.mdcopier and progress rows,SAFETY.mdcopier and progress rows drop "progress fillers planned".Tests
Real PostgreSQL (
work_integration_test.go): a 4-worker copy with one chunk pinned mid-insert and every other chunk landed is polled through the tracker —rows_copiedis exactly the source count minus the pinned chunk,rows_totalequals the source count afterANALYZE, andbytes_copied/bytes_totalequalpg_table_sizeof the shadow and the source as the test measures them (nothing writes either table while the pin holds); after the pin is released andRunreturns, the tracker reports noworkandCopier.Workreports the full row count with zero bytes. A table createdWITH (autovacuum_enabled = false)and never analyzed reportsrows_total0. A resume from watermark 500 over a 1000-row source reportsrows_copied500 androws_total1000. Asearch_paththat shadows the catalog — an impostorpg_classrow carrying a fakereltuplesfor the source OID, an emptypg_namespace, and apg_table_size(oid)decoy that records every call — leaves the counters real and the decoy uncalled, and the copy converges; unqualifying any one ofpg_class,pg_table_size, orrelationOIDsSQLfails it. A poll while the resume clear is fenced behind a straggling chunk transaction reports the copier's counters (the copier is the source before the clear, not only before the first chunk). A row count the copier cannot read (a closed pool) endsRunbefore the clear touches the shadow, as an ordinary error rather than an invariant violation, with the tracker left unregistered. A poll on a rawpgxpool.Pool— no session timeouts — queued behind anACCESS EXCLUSIVErequest on the source ends with the server's lock timeout (55P03) inside the copier's 500 ms, not the observer's patience; removing the budgets from the measurement transaction hangs it. Pure tests (work_test.go) pin the ledger-to-Workmapping and the zero state beforeRun. Seven mutants — clamp removed, tracker never registered, stop fence skipped, sizes swapped, pool kept afterRun, rows dropped, missing relation failingRun— each fail at least one test.Before / after
References
docs/progress-report.md"Work counters";docs/copy-and-swap-design.mdpackage map.🤖 Drafted with Amp (Claude Opus 4.6); reviewed and edited by the author.