Skip to content

Commit 641f211

Browse files
committed
fix(applier): disable NOWAIT for row-copy under MTS to avoid 3572 FATAL
Under MTS mode (NumWorkers > 1), parallel DML workers hold row locks on the ghost table concurrently with row-copy. SELECT ... FOR SHARE NOWAIT fails immediately with errno 3572 when it encounters those locks, and the combined retry budget (20 internal + 3 outer) is insufficient under heavy write load on MySQL 8.4, causing the migration to FATAL. Extract the noWait decision into rowCopyUsesNoWait() which returns false when NumWorkers > 1, making row-copy use LOCK IN SHARE MODE (blocking wait) instead. This is the correct trade-off: DML apply is latency-sensitive and should not be preempted by row-copy retries, so row-copy should wait for DML locks to release rather than failing fast. Single-threaded mode on MySQL 8.x retains NOWAIT behaviour unchanged.
1 parent f00e6d4 commit 641f211

2 files changed

Lines changed: 40 additions & 6 deletions

File tree

go/logic/applier.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,12 +1107,24 @@ func (apl *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange bool
11071107
}
11081108

11091109
// rowCopyMaxTransientRetries bounds internal retries of the chunk-INSERT query on
1110-
// transient lock errors (NOWAIT 3572, lock-wait timeout 1205, deadlock 1213).
1111-
// Under heavy concurrent write load the SELECT ... FOR SHARE NOWAIT clause can
1112-
// fail immediately; retrying with short backoff avoids exhausting the outer
1113-
// retryOperation budget (which defaults to only 3 attempts).
1110+
// transient lock errors (lock-wait timeout 1205, deadlock 1213). In single-threaded
1111+
// mode on MySQL 8.x, NOWAIT (3572) can also occur and is retried here.
11141112
const rowCopyMaxTransientRetries = 20
11151113

1114+
// rowCopyUsesNoWait reports whether row-copy should use SELECT ... FOR SHARE NOWAIT.
1115+
// On MySQL 8.x transactional tables, NOWAIT fails fast instead of blocking behind
1116+
// concurrent DML. Under MTS (NumWorkers > 1), lock contention with parallel DML
1117+
// workers is expected; row-copy waits via LOCK IN SHARE MODE instead.
1118+
func rowCopyUsesNoWait(migrationContext *base.MigrationContext) bool {
1119+
if !strings.HasPrefix(migrationContext.ApplierMySQLVersion, "8.") {
1120+
return false
1121+
}
1122+
if migrationContext.NumWorkers > 1 {
1123+
return false
1124+
}
1125+
return true
1126+
}
1127+
11161128
// ApplyIterationInsertQuery issues a chunk-INSERT query on the ghost table. It is where
11171129
// data actually gets copied from original table.
11181130
func (apl *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected int64, duration time.Duration, err error) {
@@ -1131,8 +1143,7 @@ func (apl *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected i
11311143
apl.migrationContext.MigrationIterationRangeMaxValues.AbstractValues(),
11321144
apl.migrationContext.GetIteration() == 0,
11331145
apl.migrationContext.IsTransactionalTable(),
1134-
// TODO: Don't hardcode this
1135-
strings.HasPrefix(apl.migrationContext.ApplierMySQLVersion, "8."),
1146+
rowCopyUsesNoWait(apl.migrationContext),
11361147
)
11371148
if err != nil {
11381149
return chunkSize, rowsAffected, duration, err

go/logic/applier_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,29 @@ func TestRowCopyMaxTransientRetries(t *testing.T) {
233233
require.Equal(t, 20, rowCopyMaxTransientRetries, "rowCopyMaxTransientRetries should be 20")
234234
}
235235

236+
func TestRowCopyUsesNoWait(t *testing.T) {
237+
t.Run("mysql8 single-threaded uses NOWAIT", func(t *testing.T) {
238+
mctx := base.NewMigrationContext()
239+
mctx.ApplierMySQLVersion = "8.4.3"
240+
mctx.NumWorkers = 1
241+
require.True(t, rowCopyUsesNoWait(mctx))
242+
})
243+
244+
t.Run("mysql8 MTS waits for locks", func(t *testing.T) {
245+
mctx := base.NewMigrationContext()
246+
mctx.ApplierMySQLVersion = "8.4.3"
247+
mctx.NumWorkers = 4
248+
require.False(t, rowCopyUsesNoWait(mctx))
249+
})
250+
251+
t.Run("mysql57 never uses NOWAIT", func(t *testing.T) {
252+
mctx := base.NewMigrationContext()
253+
mctx.ApplierMySQLVersion = "5.7.44"
254+
mctx.NumWorkers = 4
255+
require.False(t, rowCopyUsesNoWait(mctx))
256+
})
257+
}
258+
236259
func TestApplierInstantDDL(t *testing.T) {
237260
migrationContext := base.NewMigrationContext()
238261
migrationContext.DatabaseName = "test"

0 commit comments

Comments
 (0)