@@ -7,10 +7,13 @@ package base
77
88import (
99 "context"
10+ "crypto/sha256"
11+ "encoding/hex"
1012 "fmt"
1113 "math"
1214 "os"
1315 "regexp"
16+ "sort"
1417 "strings"
1518 "sync"
1619 "sync/atomic"
@@ -76,6 +79,139 @@ func NewThrottleCheckResult(throttle bool, reason string, reasonHint ThrottleRea
7679 }
7780}
7881
82+ // MoveTable holds the per-table runtime state for a single table within a
83+ // move-tables run. In move-tables mode the surrounding plumbing (one binlog
84+ // stream, one applier connection, one throttler, one hooks executor) stays
85+ // singular, but every migrated table carries its own schema, unique key,
86+ // iteration progress, and counters keyed by table name.
87+ //
88+ // The range/iteration fields are guarded by the per-table rangeMutex. The
89+ // applier-wide "current applied source coordinates" mutex stays single — there
90+ // is one applied stream feeding all tables.
91+ type MoveTable struct {
92+ // Identity.
93+ SourceDatabaseName string
94+ SourceTableName string
95+ TargetDatabaseName string
96+ TargetTableName string
97+
98+ // CreateTableStatement is the captured `SHOW CREATE TABLE` from the source,
99+ // used to (re)create the table on the target.
100+ CreateTableStatement string
101+
102+ // Schema, captured from the source (or from the target, on resume). In
103+ // move-tables mode source and target schemas match, so the shared columns are
104+ // identical to the original columns.
105+ OriginalTableColumns * sql.ColumnList
106+ OriginalTableVirtualColumns * sql.ColumnList
107+ OriginalTableUniqueKeys [](* sql.UniqueKey )
108+ UniqueKey * sql.UniqueKey
109+ SharedColumns * sql.ColumnList
110+ MappedSharedColumns * sql.ColumnList
111+
112+ // RowsEstimate is the estimated row count for this table.
113+ RowsEstimate int64
114+
115+ // Iteration / range state. Guarded by rangeMutex (except Iteration, which is
116+ // accessed atomically so status readers don't need the lock).
117+ MigrationRangeMinValues * sql.ColumnValues
118+ MigrationRangeMaxValues * sql.ColumnValues
119+ MigrationIterationRangeMinValues * sql.ColumnValues
120+ MigrationIterationRangeMaxValues * sql.ColumnValues
121+ Iteration int64
122+
123+ // LastIterationRange* record the last successfully-copied chunk range, used
124+ // for checkpointing. Guarded by rangeMutex.
125+ LastIterationRangeMinValues * sql.ColumnValues
126+ LastIterationRangeMaxValues * sql.ColumnValues
127+
128+ // RowsCopied is the number of rows copied for this table (accessed atomically).
129+ RowsCopied int64
130+
131+ // rowCopyComplete is set (1) once this table's row copy finishes. The
132+ // on-row-copy-complete hook and the cutover only proceed once every table is
133+ // complete. Accessed atomically.
134+ rowCopyComplete int64
135+
136+ // rangeMutex guards this table's range/iteration fields.
137+ rangeMutex sync.Mutex
138+ }
139+
140+ // GetIteration returns the table's current iteration counter.
141+ func (mt * MoveTable ) GetIteration () int64 {
142+ return atomic .LoadInt64 (& mt .Iteration )
143+ }
144+
145+ // IncrementIteration advances the table's iteration counter by one.
146+ func (mt * MoveTable ) IncrementIteration () {
147+ atomic .AddInt64 (& mt .Iteration , 1 )
148+ }
149+
150+ // SetNextIterationRangeMinValues advances the iteration window: the next chunk's
151+ // min becomes the previous chunk's max (or the table min for the first chunk).
152+ func (mt * MoveTable ) SetNextIterationRangeMinValues () {
153+ mt .rangeMutex .Lock ()
154+ defer mt .rangeMutex .Unlock ()
155+ mt .MigrationIterationRangeMinValues = mt .MigrationIterationRangeMaxValues
156+ if mt .MigrationIterationRangeMinValues == nil {
157+ mt .MigrationIterationRangeMinValues = mt .MigrationRangeMinValues
158+ }
159+ }
160+
161+ // IsRowCopyComplete reports whether this table has finished its row copy.
162+ func (mt * MoveTable ) IsRowCopyComplete () bool {
163+ return atomic .LoadInt64 (& mt .rowCopyComplete ) > 0
164+ }
165+
166+ // SetRowCopyComplete marks this table's row copy as finished.
167+ func (mt * MoveTable ) SetRowCopyComplete () {
168+ atomic .StoreInt64 (& mt .rowCopyComplete , 1 )
169+ }
170+
171+ // RecordLastIterationRange stores the last successfully-copied chunk range for
172+ // checkpointing.
173+ func (mt * MoveTable ) RecordLastIterationRange () {
174+ mt .rangeMutex .Lock ()
175+ defer mt .rangeMutex .Unlock ()
176+ if mt .MigrationIterationRangeMinValues != nil && mt .MigrationIterationRangeMaxValues != nil {
177+ mt .LastIterationRangeMinValues = mt .MigrationIterationRangeMinValues .Clone ()
178+ mt .LastIterationRangeMaxValues = mt .MigrationIterationRangeMaxValues .Clone ()
179+ }
180+ }
181+
182+ // GetLastIterationRange returns clones of the last successfully-copied chunk
183+ // range for checkpointing. Either value may be nil if no chunk has completed.
184+ func (mt * MoveTable ) GetLastIterationRange () (minValues , maxValues * sql.ColumnValues ) {
185+ mt .rangeMutex .Lock ()
186+ defer mt .rangeMutex .Unlock ()
187+ if mt .LastIterationRangeMinValues != nil {
188+ minValues = mt .LastIterationRangeMinValues .Clone ()
189+ }
190+ if mt .LastIterationRangeMaxValues != nil {
191+ maxValues = mt .LastIterationRangeMaxValues .Clone ()
192+ }
193+ return minValues , maxValues
194+ }
195+
196+ // GetRowsCopied returns the number of rows copied for this table.
197+ func (mt * MoveTable ) GetRowsCopied () int64 {
198+ return atomic .LoadInt64 (& mt .RowsCopied )
199+ }
200+
201+ // RestoreFromCheckpoint rehydrates this table's row-copy state from a resumed
202+ // checkpoint: the next chunk starts at the last-copied range, and the iteration
203+ // counter and rows-copied total are restored.
204+ func (mt * MoveTable ) RestoreFromCheckpoint (rangeMin , rangeMax * sql.ColumnValues , iteration , rowsCopied int64 ) {
205+ mt .rangeMutex .Lock ()
206+ mt .MigrationIterationRangeMinValues = rangeMin
207+ mt .MigrationIterationRangeMaxValues = rangeMax
208+ mt .LastIterationRangeMinValues = rangeMin
209+ mt .LastIterationRangeMaxValues = rangeMax
210+ mt .rangeMutex .Unlock ()
211+ atomic .StoreInt64 (& mt .Iteration , iteration )
212+ atomic .StoreInt64 (& mt .RowsCopied , rowsCopied )
213+ }
214+
79215// MigrationContext has the general, global state of migration. It is used by
80216// all components throughout the migration process.
81217type MigrationContext struct {
@@ -285,12 +421,15 @@ type MigrationContext struct {
285421
286422 // move tables:
287423 MoveTables struct {
288- TableNames []string // List of table names to be moved.
289- TargetHost string // Target hostname for the move. This must be a primary/writable host.
290- TargetPort int // Target MySQL port for the move.
291- TargetUser string // Target username for the move. If not specified, it will default to the source user.
292- TargetPass string // Target password for the move. If not specified, it will default to the source password.
293- TargetDatabase string // Target database name for the move. If not specified, it will default to the source database name.
424+ TableNames []string // Ordered list of table names to be moved (order from --move-tables). Iteration is deterministic over this slice, never over the Tables map.
425+ // Tables holds the per-table runtime state, keyed by source table name.
426+ // Populated by InitMoveTableContainers() once per-table schema is known.
427+ Tables map [string ]* MoveTable
428+ TargetHost string // Target hostname for the move. This must be a primary/writable host.
429+ TargetPort int // Target MySQL port for the move.
430+ TargetUser string // Target username for the move. If not specified, it will default to the source user.
431+ TargetPass string // Target password for the move. If not specified, it will default to the source password.
432+ TargetDatabase string // Target database name for the move. If not specified, it will default to the source database name.
294433
295434 // AllowOnSourcePrimary opts in to running the move-tables read path (schema
296435 // inspection, the full row copy, binlog streaming) directly against the
@@ -418,6 +557,9 @@ func getSafeTableName(baseName string, suffix string) string {
418557// GetGhostTableName generates the name of ghost table, based on original table name
419558// or a given table name
420559func (mctx * MigrationContext ) GetGhostTableName () string {
560+ if mctx .IsMoveTablesMode () {
561+ panic ("GetGhostTableName() must not be called in move-tables mode; there is no ghost table (the target keeps each migrated table's name)" )
562+ }
421563 if mctx .Revert {
422564 // When reverting the "ghost" table is the _del table from the original migration.
423565 return mctx .OldTableName
@@ -429,15 +571,6 @@ func (mctx *MigrationContext) GetGhostTableName() string {
429571 }
430572}
431573
432- // GetTargetTableName generates the name of the target table, based on original table name and
433- // the migration context (i.e. move-tables mode).
434- func (mctx * MigrationContext ) GetTargetTableName () string {
435- if mctx .IsMoveTablesMode () {
436- return mctx .MoveTables .TableNames [0 ]
437- }
438- return mctx .GetGhostTableName ()
439- }
440-
441574// GetTargetDatabaseName fetches the name of the target database, which defaults to the original
442575// database name unless we're in move-tables mode.
443576func (mctx * MigrationContext ) GetTargetDatabaseName () string {
@@ -449,6 +582,9 @@ func (mctx *MigrationContext) GetTargetDatabaseName() string {
449582
450583// GetOldTableName generates the name of the "old" table, into which the original table is renamed.
451584func (mctx * MigrationContext ) GetOldTableName () string {
585+ if mctx .IsMoveTablesMode () {
586+ panic ("GetOldTableName() must not be called in move-tables mode; use MoveTableDelName(tableName) for each migrated table's `_<table>_del` rollback handle" )
587+ }
452588 var tableName string
453589 if mctx .ForceTmpTableName != "" {
454590 tableName = mctx .ForceTmpTableName
@@ -470,9 +606,29 @@ func (mctx *MigrationContext) GetOldTableName() string {
470606 return getSafeTableName (tableName , suffix )
471607}
472608
609+ // MoveTableDelName returns the `_<table>_del` rollback-handle table name for a
610+ // specific migrated table in move-tables mode. It mirrors GetOldTableName but
611+ // for an explicit table name, so a multi-table cutover can rename every source
612+ // table in one atomic RENAME. Revert is disallowed in move-tables mode, so the
613+ // suffix is always "del".
614+ func (mctx * MigrationContext ) MoveTableDelName (tableName string ) string {
615+ suffix := "del"
616+ if mctx .TimestampOldTable {
617+ t := mctx .StartTime
618+ timestamp := fmt .Sprintf ("%d%02d%02d%02d%02d%02d" ,
619+ t .Year (), t .Month (), t .Day (),
620+ t .Hour (), t .Minute (), t .Second ())
621+ return getSafeTableName (tableName , fmt .Sprintf ("%s_%s" , timestamp , suffix ))
622+ }
623+ return getSafeTableName (tableName , suffix )
624+ }
625+
473626// GetChangelogTableName generates the name of changelog table, based on original table name
474627// or a given table name.
475628func (mctx * MigrationContext ) GetChangelogTableName () string {
629+ if mctx .IsMoveTablesMode () {
630+ panic ("GetChangelogTableName() must not be called in move-tables mode; there is no changelog table (§1.2)" )
631+ }
476632 if mctx .ForceTmpTableName != "" {
477633 return getSafeTableName (mctx .ForceTmpTableName , "ghc" )
478634 } else {
@@ -484,15 +640,13 @@ func (mctx *MigrationContext) GetChangelogTableName() string {
484640func (mctx * MigrationContext ) GetCheckpointTableName () string {
485641 if mctx .ForceTmpTableName != "" {
486642 return getSafeTableName (mctx .ForceTmpTableName , "ghk" )
487- } else {
488- return getSafeTableName (mctx .OriginalTableName , "ghk" )
489643 }
490- }
491-
492- // GetVoluntaryLockName returns a name of a voluntary lock to be used throughout
493- // the swap-tables process.
494- func ( mctx * MigrationContext ) GetVoluntaryLockName () string {
495- return fmt . Sprintf ( "%s.%s.lock" , mctx .DatabaseName , mctx . OriginalTableName )
644+ if mctx . IsMoveTablesMode () {
645+ // One checkpoint table per run, named from the set-derived run token so it
646+ // does not depend on any single migrated table and is stable across resume.
647+ return getSafeTableName ( "gho_" + mctx . MoveTablesRunToken (), "ghk" )
648+ }
649+ return getSafeTableName ( mctx .OriginalTableName , "ghk" )
496650}
497651
498652// RequiresBinlogFormatChange is `true` when the original binlog format isn't `ROW`
@@ -1189,6 +1343,84 @@ func (mctx *MigrationContext) IsMoveTablesMode() bool {
11891343 return len (mctx .MoveTables .TableNames ) > 0
11901344}
11911345
1346+ // InitMoveTableContainers builds (or rebuilds) the per-table runtime containers
1347+ // from the ordered MoveTables.TableNames list. It is idempotent: tables already
1348+ // present in the map keep their existing container so callers may invoke it
1349+ // after partially populating state. Source and target table names match in
1350+ // move-tables mode; only the database may differ.
1351+ func (mctx * MigrationContext ) InitMoveTableContainers () {
1352+ if mctx .MoveTables .Tables == nil {
1353+ mctx .MoveTables .Tables = make (map [string ]* MoveTable , len (mctx .MoveTables .TableNames ))
1354+ }
1355+ for _ , tableName := range mctx .MoveTables .TableNames {
1356+ if _ , ok := mctx .MoveTables .Tables [tableName ]; ok {
1357+ continue
1358+ }
1359+ mctx .MoveTables .Tables [tableName ] = & MoveTable {
1360+ SourceDatabaseName : mctx .DatabaseName ,
1361+ SourceTableName : tableName ,
1362+ TargetDatabaseName : mctx .GetTargetDatabaseName (),
1363+ TargetTableName : tableName ,
1364+ }
1365+ }
1366+ }
1367+
1368+ // GetMoveTable returns the per-table container for the given source table name,
1369+ // or nil if it has not been initialized.
1370+ func (mctx * MigrationContext ) GetMoveTable (tableName string ) * MoveTable {
1371+ if mctx .MoveTables .Tables == nil {
1372+ return nil
1373+ }
1374+ return mctx .MoveTables .Tables [tableName ]
1375+ }
1376+
1377+ // OrderedMoveTables returns the per-table containers in --move-tables order.
1378+ // Iteration must always use this deterministic order, never the Tables map's
1379+ // (random) iteration order.
1380+ func (mctx * MigrationContext ) OrderedMoveTables () []* MoveTable {
1381+ tables := make ([]* MoveTable , 0 , len (mctx .MoveTables .TableNames ))
1382+ for _ , tableName := range mctx .MoveTables .TableNames {
1383+ if mt := mctx .GetMoveTable (tableName ); mt != nil {
1384+ tables = append (tables , mt )
1385+ }
1386+ }
1387+ return tables
1388+ }
1389+
1390+ // MoveTablesRunToken returns a short, stable identifier for a move-tables run,
1391+ // derived from the (sorted) set of migrated table names. It is:
1392+ // - deterministic: the same table set always yields the same token, so a
1393+ // resumed run finds the same run-wide artifacts (e.g. the checkpoint table).
1394+ // - order-independent: --move-tables=a,b and --move-tables=b,a match.
1395+ // - fixed-length: independent of how many tables are moved (so it never blows
1396+ // past identifier length limits the way a concatenation of names would).
1397+ //
1398+ // It is used to name run-wide singular artifacts (checkpoint table, applier
1399+ // advisory lock, serve socket) so they never depend on any single migrated
1400+ // table name. Returns "" outside move-tables mode.
1401+ func (mctx * MigrationContext ) MoveTablesRunToken () string {
1402+ if ! mctx .IsMoveTablesMode () {
1403+ return ""
1404+ }
1405+ names := append ([]string (nil ), mctx .MoveTables .TableNames ... )
1406+ sort .Strings (names )
1407+ // NUL separator: table names cannot contain it, so the join is unambiguous.
1408+ sum := sha256 .Sum256 ([]byte (strings .Join (names , "\x00 " )))
1409+ return hex .EncodeToString (sum [:6 ]) // 12 hex chars / 48 bits
1410+ }
1411+
1412+ // AllMoveTablesRowCopyComplete reports whether every migrated table has finished
1413+ // its row copy. The on-row-copy-complete hook and the cutover only proceed once
1414+ // this is true.
1415+ func (mctx * MigrationContext ) AllMoveTablesRowCopyComplete () bool {
1416+ for _ , mt := range mctx .OrderedMoveTables () {
1417+ if ! mt .IsRowCopyComplete () {
1418+ return false
1419+ }
1420+ }
1421+ return true
1422+ }
1423+
11921424// SendWithContext attempts to send a value to a channel, but returns early
11931425// if the context is cancelled. This prevents goroutine deadlocks when the
11941426// channel receiver has exited due to an error.
0 commit comments