Skip to content

Commit 20111ae

Browse files
committed
corrections after review and testing
1 parent eaa41cd commit 20111ae

8 files changed

Lines changed: 44 additions & 39 deletions

File tree

doc/command-line-flags.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ Controls how many rows `gh-ost` copies in each row-copy iteration. The default i
110110

111111
Parallel copy requires checkpoints because parallel INSERTs can go out of sequential order and leave gaps in target table. gh-ost checkpoints are consistent and do not have gaps, so resuming from a checkpoint will make the target table consistent again.
112112

113-
Consistency is ensured by using
114-
-INSERT IGNORE/SELECT FOR SHARE for row copy (current gh-ost behavior)
115-
-DELETE/INSERT by DML applier when unique key change is detected (current gh-ost behavior)
116-
-advanceFrontier call to ensure that checkpoints have no gaps (used by parallel-copy only)
113+
Consistency is ensured by using:
114+
- `INSERT IGNORE`/`SELECT FOR SHARE` for row copy (current gh-ost behavior)
115+
- `DELETE`/`INSERT` by DML applier when unique key change is detected (current gh-ost behavior)
116+
- `advanceFrontier` call to ensure that checkpoints have no gaps (used by parallel-copy only)
117117
Without all of those, consistency of parallel copy cannot be guaranteed (for example, row copy could insert row back after it was deleted). In existing serial mode, the problem is much easier because DML applier is never parallel with row copy.
118118

119119
### parallel-copy-workers

go/base/context.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,16 @@ const (
5656
MaxEventsBatchSize = 1000
5757
ETAUnknown = math.MinInt64
5858

59-
// MaxCopyWorkers is the hard upper bound on --parallel-copy-workers. The applier
60-
// connection pool is sized to CopyWorkers + ParallelCopyConnHeadroom, so this
61-
// also bounds connection usage and prevents exhausting the server's
59+
// MaxParallelCopyWorkers is the hard upper bound on --parallel-copy-workers. The
60+
// applier connection pool is sized to ParallelCopyWorkers + ParallelCopyConnHeadroom,
61+
// so this also bounds connection usage and prevents exhausting the server's
6262
// max_connections.
63-
MaxCopyWorkers = 64
64-
// RecommendedMaxCopyWorkers is a soft threshold above which we warn: the
63+
MaxParallelCopyWorkers = 64
64+
// RecommendedMaxParallelCopyWorkers is a soft threshold above which we warn: the
6565
// throughput benefit of more workers plateaus while load keeps growing.
66-
RecommendedMaxCopyWorkers = 16
66+
RecommendedMaxParallelCopyWorkers = 16
6767
// ParallelCopyConnHeadroom is the number of connections added to the applier
68-
// pool on top of CopyWorkers, to cover the DML event applier goroutine
68+
// pool on top of ParallelCopyWorkers, to cover the DML event applier goroutine,
6969
// the status logger, the heartbeat goroutine, and the checkpoint loop.
7070
ParallelCopyConnHeadroom = 4
7171
)

go/cmd/gh-ost/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,13 @@ func main() {
347347
if migrationContext.ParallelCopyWorkers < 1 {
348348
migrationContext.Log.Fatalf("--parallel-copy-workers should be >=1")
349349
}
350-
if migrationContext.ParallelCopyWorkers > base.MaxCopyWorkers {
351-
migrationContext.Log.Fatalf("--parallel-copy-workers should be <=%d", base.MaxCopyWorkers)
350+
if migrationContext.ParallelCopyWorkers > base.MaxParallelCopyWorkers {
351+
migrationContext.Log.Fatalf("--parallel-copy-workers should be <=%d", base.MaxParallelCopyWorkers)
352352
}
353-
if migrationContext.ParallelCopyWorkers > base.RecommendedMaxCopyWorkers {
354-
migrationContext.Log.Warningf("--parallel-copy-workers=%d is high: throughput gains tend to plateau while load on the server grows. Consider <=%d.", migrationContext.ParallelCopyWorkers, base.RecommendedMaxCopyWorkers)
353+
if migrationContext.ParallelCopyWorkers > base.RecommendedMaxParallelCopyWorkers {
354+
migrationContext.Log.Warningf("--parallel-copy-workers=%d is high: throughput gains tend to plateau while load on the server grows. Consider <=%d.", migrationContext.ParallelCopyWorkers, base.RecommendedMaxParallelCopyWorkers)
355355
}
356-
if migrationContext.ParallelCopyMaxHeartbeatLagThresholdMillies > 0 && migrationContext.ParallelCopyMaxHeartbeatLagThresholdMillies < 100 {
356+
if migrationContext.ParallelCopyMaxHeartbeatLagThresholdMillies != 0 && migrationContext.ParallelCopyMaxHeartbeatLagThresholdMillies < 100 {
357357
migrationContext.Log.Fatalf("--parallel-copy-max-heartbeatlag-millis should be >=100 or 0 (disabled)")
358358
}
359359
}

go/logic/applier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,7 @@ func (apl *Applier) ApplyIterationInsertQuery(parallelRange ...*iterationRange)
11781178
if parallel {
11791179
// Use the explicitly-passed range. The parallel INSERT runs unlocked and
11801180
// concurrently, so it must NOT read the shared MigrationIterationRange* cursor:
1181-
// another worker is mutating it under parallelSelectMutex.
1181+
// another worker is mutating it under parallelCopySelectMutex.
11821182
rangeMin = parallelRange[0].min
11831183
rangeMax = parallelRange[0].max
11841184
includeRangeStartValues = parallelRange[0].includeRangeStart

go/logic/migrator.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,7 +1520,7 @@ func (mgtr *Migrator) printStatus(rule PrintStatusRule, snap migrationProgressSn
15201520

15211521
throughputSuffix := ""
15221522
if mgtr.migrationContext.ParallelCopy {
1523-
throughputSuffix = fmt.Sprintf("; Throughput: %drows/sec", atomic.LoadInt64(&mgtr.migrationContext.EtaRowsPerSecond))
1523+
throughputSuffix = fmt.Sprintf("; Throughput: %d rows/sec", atomic.LoadInt64(&mgtr.migrationContext.EtaRowsPerSecond))
15241524
}
15251525
status := fmt.Sprintf("Copy: %d/%d %.1f%%; Applied: %d; Backlog: %d/%d; Time: %+v(total), %+v(copy); streamer: %+v; Lag: %.2fs, HeartbeatLag: %.2fs, State: %s; ETA: %s%s",
15261526
snap.totalRowsCopied, snap.rowsEstimate, snap.progressPct,
@@ -1708,7 +1708,7 @@ func (mgtr *Migrator) iterateChunks() error {
17081708
return nil
17091709
}
17101710
// --parallel-copy state, captured once per copyRowsFunc invocation under
1711-
// parallelSelectMutex so the chunk INSERT (which runs unlocked, in parallel) uses
1711+
// parallelCopySelectMutex so the chunk INSERT (which runs unlocked, in parallel) uses
17121712
// a stable range even as other workers advance the shared cursor, and so a retry
17131713
// re-inserts the same chunk rather than re-scanning a new range.
17141714
var parallelIteration int64
@@ -1720,10 +1720,14 @@ func (mgtr *Migrator) iterateChunks() error {
17201720
}
17211721
// Copy task:
17221722
applyCopyRowsFunc := func() error {
1723-
if atomic.LoadInt64(&mgtr.rowCopyCompleteFlag) == 1 || atomic.LoadInt64(&hasNoFurtherRangeFlag) == 1 {
1724-
// Done.
1725-
// There's another such check down the line
1726-
return nil
1723+
// Once this closure has captured a chunk range, it must keep retrying that
1724+
// same INSERT to completion even if hasNoFurtherRangeFlag/rowCopyCompleteFlag is set
1725+
if !parallelCaptured {
1726+
if atomic.LoadInt64(&mgtr.rowCopyCompleteFlag) == 1 || atomic.LoadInt64(&hasNoFurtherRangeFlag) == 1 {
1727+
// Done.
1728+
// There's another such check down the line
1729+
return nil
1730+
}
17271731
}
17281732

17291733
if mgtr.migrationContext.ParallelCopy {
@@ -1733,7 +1737,7 @@ func (mgtr *Migrator) iterateChunks() error {
17331737
// advanceFrontier is never left with an abandoned iteration.
17341738
if !parallelCaptured {
17351739
var stopErr error
1736-
parallelIteration, parallelRangeMin, parallelRangeMax, parallelCaptured, stopErr = mgtr.captureParallelChunkRange(&hasNoFurtherRangeFlag, terminateRowIteration)
1740+
parallelIteration, parallelRangeMin, parallelRangeMax, parallelCaptured, stopErr = mgtr.captureParallelChunkRange(&hasNoFurtherRangeFlag)
17371741
if !parallelCaptured {
17381742
return stopErr
17391743
}
@@ -1819,9 +1823,9 @@ func (mgtr *Migrator) iterateChunks() error {
18191823
}
18201824

18211825
// Enqueue copy operation. In serial mode it is consumed by executeWriteFuncs(); in
1822-
// --parallel-copy mode it is consumed by one of the CopyWorkers workers started in
1823-
// copyRowsParallel(). The closure itself serializes its boundary SELECT under
1824-
// parallelSelectMutex, so the only thing parallelized is the chunk INSERT.
1826+
// --parallel-copy mode it is consumed by one of the ParallelCopyWorkers worker
1827+
// goroutines started in copyRowsParallel(). The closure itself serializes its boundary SELECT under
1828+
// parallelCopySelectMutex, so the only thing parallelized is the chunk INSERT.
18251829
// Use helper to prevent deadlock if the consumer exits.
18261830
if err := base.SendWithContext(mgtr.migrationContext.GetContext(), mgtr.copyRowsQueue, copyRowsFunc); err != nil {
18271831
// Context cancelled, check for abort and exit
@@ -1833,13 +1837,14 @@ func (mgtr *Migrator) iterateChunks() error {
18331837
}
18341838
}
18351839

1836-
// captureParallelChunkRange acquires parallelSelectMutex, advances the shared scan
1837-
// cursor, and captures the next chunk's range. Returns captured=true with the range
1838-
// values on success. Returns captured=false with nil err when no further work is needed
1839-
// (row copy already complete or table exhausted; in the latter case terminateRowIteration
1840-
// is called to signal the channel). Returns captured=false with non-nil err when the
1841-
// range scan itself fails; the caller should propagate it to trigger a retry.
1842-
func (mgtr *Migrator) captureParallelChunkRange(hasNoFurtherRangeFlag *int64, terminateRowIteration func(error) error) (iteration int64, rangeMin, rangeMax *sql.ColumnValues, captured bool, err error) {
1840+
// This helper never signals clean completion on rowCopyComplete itself: that signal must
1841+
// fire exactly once, after copyRowsParallel's wg.Wait() confirms every worker goroutine
1842+
// has actually returned — not merely that the scan is exhausted. Signaling here, as soon
1843+
// as no further range is found, would let the migrator start moving toward cutover while
1844+
// other workers may still be retrying an in-flight INSERT for a chunk they already
1845+
// captured. (That a captured chunk is never dropped once other workers see this flag is a
1846+
// separate guarantee, enforced by applyCopyRowsFunc's parallelCaptured guard.)
1847+
func (mgtr *Migrator) captureParallelChunkRange(hasNoFurtherRangeFlag *int64) (iteration int64, rangeMin, rangeMax *sql.ColumnValues, captured bool, err error) {
18431848
mgtr.parallelCopySelectMutex.Lock()
18441849
if atomic.LoadInt64(&mgtr.rowCopyCompleteFlag) == 1 || atomic.LoadInt64(hasNoFurtherRangeFlag) == 1 {
18451850
mgtr.parallelCopySelectMutex.Unlock()
@@ -1854,7 +1859,7 @@ func (mgtr *Migrator) captureParallelChunkRange(hasNoFurtherRangeFlag *int64, te
18541859
if !hasFurtherRange {
18551860
atomic.StoreInt64(hasNoFurtherRangeFlag, 1)
18561861
mgtr.parallelCopySelectMutex.Unlock()
1857-
return 0, nil, nil, false, terminateRowIteration(nil)
1862+
return 0, nil, nil, false, nil
18581863
}
18591864
iteration = atomic.LoadInt64(&mgtr.parallelCopyDispatchSeq)
18601865
rangeMin = mgtr.migrationContext.MigrationIterationRangeMinValues.Clone()

go/logic/parallel_copy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func (mgtr *Migrator) throttleOnHeartbeatLag(ctx context.Context) {
5757

5858
// copyRowsParallel is the --parallel-copy consumer side. iterateChunks remains the single
5959
// producer, enqueuing copy-task closures onto copyRowsQueue exactly as in serial mode; this
60-
// function starts CopyWorkers consumer goroutines that pull those closures and run them
61-
// concurrently. Each closure serializes its own boundary SELECT under parallelSelectMutex
60+
// function starts ParallelCopyWorkers consumer goroutines that pull those closures and run them
61+
// concurrently. Each closure serializes its own boundary SELECT under parallelCopySelectMutex
6262
// (so chunk ranges are still produced sequentially) and only the chunk INSERT runs in
6363
// parallel; advanceFrontier then commits global state in contiguous iteration order so a
6464
// crash/resume with checkpoints never leaves un-copied holes.

localtests/parallel-copy-resume/create.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ insert into gh_ost_test (i, color) values
1717
-- run spends well over one checkpoint interval (10s) in row-copy. This makes it
1818
-- likely that a checkpoint lands mid-copy, so --resume exercises gap-filling
1919
-- from a partial frontier. (The test stays correct either way; the deterministic
20-
-- gap logic is covered by go/logic/parallel_test.go.)
20+
-- gap logic is covered by go/logic/parallel_copy_test.go.)
2121
insert into gh_ost_test (i, color) select i, color from gh_ost_test;
2222
insert into gh_ost_test (i, color) select i, color from gh_ost_test;
2323
insert into gh_ost_test (i, color) select i, color from gh_ost_test;

localtests/parallel-copy-resume/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# The test is robust to timing: if the first run happens to finish the row copy
77
# before we kill it, --resume simply has no gaps to fill and completes. Either
88
# way the ghost table must match the original. The deterministic gap-filling
9-
# logic itself is covered by the unit tests in go/logic/parallel_test.go.
9+
# logic itself is covered by the unit tests in go/logic/parallel_copy_test.go.
1010

1111
table_name="gh_ost_test"
1212
ghost_table_name="_gh_ost_test_gho"

0 commit comments

Comments
 (0)