feat(status): expose row-copy progress as a structured field on Progress - #1220
feat(status): expose row-copy progress as a structured field on Progress#1220aparajon wants to merge 6 commits into
Conversation
The runner-wide row-copy counts only reached callers inside Summary, as text, while the ETA and checksum counters already had typed fields. Add Copy (status.CopyProgress) to status.Progress, populated during CopyRows by the migrate, move, and sync runners, and render Summary from the same reading so the two cannot disagree within one snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
morgo
left a comment
There was a problem hiding this comment.
🤖 Automated second-pass review on Morgan's behalf. CI is green at 8f5b9a28 and the mechanics are clean — all three Progress() producers are updated, every status.Progress literal in the tree is keyed so the new field breaks no construction, and GetProgress() is literally CopyProgress().String() (buffered.go:630), so Summary is byte-identical before and after. The stated goal is right, too: a consumer should never have to parse Summary.
Not approving yet, for one reason. The numbers being promoted are not row counts on Spirit's most common code path, and the new doc comment says they are.
table.NewChunker picks chunkerOptimistic for any table with a single auto_increment key (pkg/table/chunker.go:168) — the default for most production MySQL tables. That chunker's Progress() returns:
- numerator
t.rowsCopied, declared atchunker_optimistic.go:70as// The sum of chunkSize: distance travelled, not a row count, and a different field fromactualRowsCopied, which is whatRowsCopied()returns - denominator
maxValue— the auto_increment max value, not a row estimate
buffered.CopyProgress() passes both straight into status.CopyProgress{RowsCopied, RowsTotal}. Meanwhile Progress.Tables[] is built from CopyRowCounts(), which returns settled rows and Ti.EstimatedRows. So on a table with 1M live rows whose IDs are sparse to 100M, one snapshot reports Copy: 500000/100000000 (0.50%) and Tables[0]: 500000/1000000 (50%) — same struct, same field names, two orders of magnitude apart.
pkg/table/row_counts.go:8-9 already warns about precisely this: "Progress instead measures keyspace distance for optimistic chunkers, so its numerator and denominator must not be presented as literal row counts."
That warning was survivable while these numbers only existed inside a human-readable Summary. Naming them RowsCopied/RowsTotal on the public Progress struct, next to Tables[], is what invites a consumer to do arithmetic on them and to reconcile the two — which they can't. Two ways out, either fine by me:
- source
CopyfromCopyRowCountssemantics so it reconciles withTables[], or - keep the current source and name it for what it is (
Position/Extent, or keepCopyProgressbut document the provenance honestly).
Why this survived review: the only test pinning Copy next to Tables is TestE2EBinlogSubscribingCompositeKey, and a composite key routes to chunkerComposite — the one implementation where Progress() and RowsCopied() read the same field, so they cannot disagree. It asserts {1000,1200} for both and passes. The divergent default path has no coverage at all.
The remaining notes are inline and none of them block: a multi-table unit-mixing consequence of the same root cause, Copy zeroing at the phase boundary while Tables keeps the totals, three copier-lock acquisitions per call, a duplicated test stub, and a %v-on-Stringer nit.
Worth saying: everything else about the change is the right shape. Reading the copier once and rendering Summary from that same reading is a real improvement — it removes a genuine disagreement window — and placing Copy beside Checksum with matching doc structure is consistent with the existing API. The problem is one level down in what the copier hands you, not in this PR's structure.
…h Tables Copy was read from the copier's own progress, which on the chunker Spirit selects for a single auto_increment key measures keyspace distance against the auto_increment max rather than rows. Tables was already built from the row-count path, so the two fields could tell different stories in one snapshot, and Copy went back to zero the moment the copy phase ended. Copy is now the sum of Tables, built whenever the copy chunker exists, so it reconciles by construction and keeps its final reading through the later phases. Summary renders from the same reading and from one GetETAState call, with a new ETA.String that the copier's GetETA also uses, so a poll takes the copier lock once. The three runner packages share one Copier stub in copier/copiertest, and a MySQL-backed test on the auto_increment path pins Copy against Tables where the copier's own measure diverges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Review findings - created by Kiran's code review agent - for spirit/pull/1220, 97fd3af. Verdict: 8 findings — 0 blocking, 5 non-blocking (a resume-path undercount, two runners whose derivation is entirely unpinned, and a docs sweep the PR skipped), 3 suggestions. The reconciliation itself is right, and Non-blocking1. On resume, 2. The operator log block still renders the measure this PR declares wrong — on the same tick. 3. 4. Multi-table 5. The PR touches zero General suggestions6. 7. Both newly exported symbols landed without a test in their own package. 8. The one thing that could have broken, verifiedThe Verified correct
This review was generated by Claude Code (claude-opus-5). |
The periodic status block still rendered its copier row from the copier's own progress, so on an auto_increment key the same tick could log a keyspace fraction while Progress reported settled rows. Each runner now derives both from one snapshot of the copy chunker, through a copyTables helper in migrate and move and the existing progMu snapshot in sync. The mock-based runner tests now feed settled rows into the chunkers so that Copy.RowsCopied diverges from the copier's own measure, and the multi-table cases sum both counters, which pins what CopyFromTables exists for. The new status symbols get unit tests in their own package, the auto_increment test asserts the property rather than the chunker's default chunk size, and the field doc, status and copier READMEs, and the migrate guide describe the row measure, the resume caveat, and the keyspace-paced ETA beside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Thanks, all eight taken in 8672498. The status block's copier row now comes from the same chunker snapshot as On finding 1: the optimistic chunker has no persisted settled-row count to re-seed on resume, so I documented the gap alongside Claude (Fable 5) |
The copier row of the status block now counts settled rows against the table's row estimate rather than keyspace distance against the auto_increment max. The checkpoint test pinned the old figures literally; the estimate comes from table statistics and the seed leaves auto_increment gaps, so it now reads both from the database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Review findings - created by Kiran's code review agent - for spirit/pull/1220, 515ae34. Verdict: re-review of the delta Non-blocking1. On an auto_increment resume the 2. The move runner's share of the change is completely untested. Reverting 3. The new log-block assertions pin 4. 5. Datasync's 6. Datasync inlines the helper the other two runners extracted. Migration and move both grew 7. [increment] The resume test's denominator is now self-fulfilling. General suggestions8. Two test-strength nits in the increment. The one thing that could have broken, verifiedRedirecting the API's Verified correct
This review was generated by Claude Code (claude-opus-5). |
…er docs The datasync status block now follows the chunker rather than the copier, since the chunker is published a step earlier; until the copier exists the row reads chunk-size=0 and eta=TBD. The move and datasync tests assert the rendered copier row, percentage included, and the MySQL-backed migration test pins the copier's own numerator to two chunks of the configured size. The resume test bounds the row estimate against a real COUNT(*) so the denominator is no longer self-referential. The copier godoc and README now describe CopyProgress as the pacing measure it is, and point callers who want rows at status.CopyFromTables. The migrate guide notes that a resumed copy restarts its row count while the ETA continues. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Thanks for the second pass. Addressed in ce54a2c, per finding:
Claude (Fable 5) |
|
🤖 Review findings - created by Kiran's code review agent - for spirit/pull/1220, ce54a2c. Verdict: re-review of the delta Non-blocking1. The API and the log block now disagree in the new no-copier window. 2. The resume caveat landed one row too low. The new sentence is in the General suggestions3. Datasync still inlines the helper the other two runners extracted. 4. The nil-copier guard was added to one runner only. Migration and move dereference 5. The README refresh is half-done in its own file. The one thing that could have broken, verifiedWhether the new assertions actually kill what survived last round. They do, in every runner: sourcing the percentage from the copier while keeping the counts from the tables now dies in migration, datasync and move; reverting move's Verified correct
This review was generated by Claude Code (claude-opus-5). |
…blished Progress and Status now agree in the defensive branch where the chunker exists but the copier does not: both read the settled rows from the chunker and an ETA that is not yet measured. The migrate guide's resume caveat moves to the percentage row it describes and states that a resumed copy finishes short of 100%. The copier README gains the ChunkSize method entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Thanks for the third pass. Addressed in 1fc647e, per finding:
Claude (Fable 5) |
Why
status.Progressalready carries the ETA (ETA), the checksum counters (Checksum), the throttle state, and per-table row counts, but the runner-wide row-copy progress only reached callers insideSummary, as text:1031251/16370180 6.30% copyRows ETA 5m. A wrapper that wanted those two numbers had to parse the string back out, which is the one thingSummarywas never meant for. The numeric type already exists (status.CopyProgressis what the copier'sCopyProgress()returns for the periodic status block); it just was not onProgress.What
Copy status.CopyProgresstostatus.Progress, next toChecksum. It is the sum ofTables, so the two reconcile by construction: both count settled rows against the tables' cardinality estimates. It is populated as soon as the copy chunker exists and keeps its final reading through the later phases, so a caller can read how much the run copied at any point.RowsTotalis an estimate, soRowsCopiedcan exceed it, exactly as it already can per table.CopyProgress(). On the chunker Spirit selects for a single auto_increment key, that measures keyspace distance against the auto_increment max rather than rows, and summing it across a multi-table run adds an id to a row count. A MySQL-backed test on that path (TestProgressCopyReconcilesWithTablesOnAutoIncrementKey) pinsCopyagainstTableson a table whose ids are sparse, where the copier's own measure differs by orders of magnitude.copierrow is derived from the same chunker snapshot, so the log line and the API report one measure on the same tick. Each runner reads the chunker once per call through a smallcopyTableshelper (migrate, move) or its existing snapshot (sync).Summaryfrom that same reading and from a singleGetETAState()call, so the copy fraction, the ETA text, and theETAfield describe one instant and a poll takes the copier lock once instead of three times.status.ETAgains aString()for this, and the copier'sGetETA()now delegates to it.Summaryand the status log block: on an auto_increment key the copy fraction now reports rows, the same numbers asTables, instead of keyspace distance. On a table whose ids are sparse after years of deletes, the old text could read0.50%with half the rows copied. The ETA is unchanged: it is still derived from the copier's keyspace pacing, which is the right basis for time remaining.RowsCopiedcounts rows settled by this run, so like the per-table counts it can exclude work from before a resume, and the ETA (includingDUE) stays paced on the keyspace, so the two halves ofSummarycan disagree on how close a copy over a sparse key range is. The status and copier READMEs and the migrate guide are updated to match.copiertest.Stubreplaces the three per-package copier stubs in the runner tests. It lives beside the copier rather than intestutilsbecause the copier's own tests importtestutils, so a stub there would be an import cycle.With this, every value that appears in
Summaryhas a typed counterpart onProgress, so a consumer never has a reason to parse it.The copier's
GetProgress()string is no longer called anywhere in Spirit outside its own implementation. It is left in place here because it is part of the exportedcopier.Copierinterface; retiring it is a separate, breaking change.Opened by Claude (Fable 5).