Skip to content

Commit a6d682a

Browse files
Merge pull request #1726 from github/multitable-spike
Move-tables. Multi-table
2 parents a0f8722 + 8738524 commit a6d682a

30 files changed

Lines changed: 2512 additions & 473 deletions

File tree

go/base/context.go

Lines changed: 255 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ package base
77

88
import (
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.
81217
type 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
420559
func (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.
443576
func (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.
451584
func (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.
475628
func (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 {
484640
func (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.

go/base/context_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,44 @@ func TestGetTableNames(t *testing.T) {
6161
}
6262
}
6363

64+
func TestMoveTableDelName(t *testing.T) {
65+
context := NewMigrationContext()
66+
// Per-table `_<table>_del` rollback handle, independent of any other table.
67+
require.Equal(t, "_some_table_del", context.MoveTableDelName("some_table"))
68+
require.Equal(t, "_other_del", context.MoveTableDelName("other"))
69+
70+
// Honors --timestamp-old-table like the single-table GetOldTableName does.
71+
context.TimestampOldTable = true
72+
longForm := "Jan 2, 2006 at 3:04pm (MST)"
73+
context.StartTime, _ = time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
74+
require.Equal(t, "_some_table_20130203195400_del", context.MoveTableDelName("some_table"))
75+
}
76+
77+
func TestMoveTablesRunToken(t *testing.T) {
78+
// Empty outside move-tables mode.
79+
require.Equal(t, "", NewMigrationContext().MoveTablesRunToken())
80+
81+
context := NewMigrationContext()
82+
context.MoveTables.TableNames = []string{"a", "b", "c"}
83+
token := context.MoveTablesRunToken()
84+
// Fixed length, lowercase hex (12 chars / 48 bits).
85+
require.Len(t, token, 12)
86+
require.Regexp(t, "^[0-9a-f]{12}$", token)
87+
// Deterministic: the same set always yields the same token (so a resumed run
88+
// finds the same run-wide artifacts).
89+
require.Equal(t, token, context.MoveTablesRunToken())
90+
91+
// Order-independent: --move-tables=a,b,c and =c,b,a match.
92+
reordered := NewMigrationContext()
93+
reordered.MoveTables.TableNames = []string{"c", "b", "a"}
94+
require.Equal(t, token, reordered.MoveTablesRunToken())
95+
96+
// A different set yields a different token.
97+
different := NewMigrationContext()
98+
different.MoveTables.TableNames = []string{"a", "b", "d"}
99+
require.NotEqual(t, token, different.MoveTablesRunToken())
100+
}
101+
64102
func TestGetTriggerNames(t *testing.T) {
65103
{
66104
context := NewMigrationContext()

0 commit comments

Comments
 (0)