Skip to content

Commit aa93172

Browse files
authored
Merge branch 'master' into grodowski/remove-unused-code
2 parents cb479a1 + ec02c37 commit aa93172

File tree

15 files changed

+484
-36
lines changed

15 files changed

+484
-36
lines changed

doc/hooks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ The following variables are available on all hooks:
7676
- `GH_OST_HOOKS_HINT_OWNER` - copy of `--hooks-hint-owner` value
7777
- `GH_OST_HOOKS_HINT_TOKEN` - copy of `--hooks-hint-token` value
7878
- `GH_OST_DRY_RUN` - whether or not the `gh-ost` run is a dry run
79+
- `GH_OST_REVERT` - whether or not `gh-ost` is running in revert mode
7980

8081
The following variable are available on particular hooks:
8182

doc/resume.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- The first `gh-ost` process was invoked with `--checkpoint`
55
- The first `gh-ost` process had at least one successful checkpoint
66
- The binlogs from the last checkpoint's binlog coordinates still exist on the replica gh-ost is inspecting (specified by `--host`)
7+
- The checkpoint table (name ends with `_ghk`) still exists
78

89
To resume, invoke `gh-ost` again with the same arguments with the `--resume` flag.
910

doc/revert.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Reverting Migrations
2+
3+
`gh-ost` can attempt to revert a previously completed migration if the follow conditions are met:
4+
- The first `gh-ost` process was invoked with `--checkpoint`
5+
- The checkpoint table (name ends with `_ghk`) still exists
6+
- The binlogs from the time of the migration's cut-over still exist on the replica gh-ost is inspecting (specified by `--host`)
7+
8+
To revert, find the name of the "old" table from the original migration e.g. `_mytable_del`. Then invoke `gh-ost` with the same arguments and the flags `--revert` and `--old-table="_mytable_del"`.
9+
gh-ost will read the binlog coordinates of the original cut-over from the checkpoint table and bring the old table up to date. Then it performs another cut-over to complete the reversion.
10+
Note that the checkpoint table (name ends with _ghk) will not be automatically dropped unless `--ok-to-drop-table` is provided.
11+
12+
> [!WARNING]
13+
> It is recommended use `--checkpoint` with `--gtid` enabled so that checkpoint binlog coordinates store GTID sets rather than file positions. In that case, `gh-ost` can revert using a different replica than it originally attached to.
14+
15+
### ❗ Note ❗
16+
Reverting is roughly equivalent to applying the "reverse" migration. _Before attempting to revert you should determine if the reverse migration is possible and does not involve any unacceptable data loss._
17+
18+
For example: if the original migration drops a `NOT NULL` column that has no `DEFAULT` then the reverse migration adds the column. In this case, the reverse migration is impossible if rows were added after the original cut-over and the revert will fail.
19+
Another example: if the original migration modifies a `VARCHAR(32)` column to `VARCHAR(64)`, the reverse migration truncates the `VARCHAR(64)` column to `VARCHAR(32)`. If values were inserted with length > 32 after the cut-over then the revert will fail.
20+
21+
22+
## Example
23+
The migration starts with a `gh-ost` invocation such as:
24+
```shell
25+
gh-ost \
26+
--chunk-size=100 \
27+
--host=replica1.company.com \
28+
--database="mydb" \
29+
--table="mytable" \
30+
--alter="drop key idx1"
31+
--gtid \
32+
--checkpoint \
33+
--checkpoint-seconds=60 \
34+
--execute
35+
```
36+
37+
In this example `gh-ost` writes a cut-over checkpoint to `_mytable_ghk` after the cut-over is successful. The original table is renamed to `_mytable_del`.
38+
39+
Suppose that dropping the index causes problems, the migration can be revert with:
40+
```shell
41+
# revert migration
42+
gh-ost \
43+
--chunk-size=100 \
44+
--host=replica1.company.com \
45+
--database="mydb" \
46+
--table="mytable" \
47+
--old-table="_mytable_del"
48+
--gtid \
49+
--checkpoint \
50+
--checkpoint-seconds=60 \
51+
--revert \
52+
--execute
53+
```
54+
55+
gh-ost then reconnects at the binlog coordinates stored in the cut-over checkpoint and applies DMLs until the old table is up-to-date.
56+
Note that the "reverse" migration is `ADD KEY idx(...)` so there is no potential data loss to consider in this case.

go/base/context.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ type MigrationContext struct {
104104
AzureMySQL bool
105105
AttemptInstantDDL bool
106106
Resume bool
107+
Revert bool
108+
OldTableName string
107109

108110
// SkipPortValidation allows skipping the port validation in `ValidateConnection`
109111
// This is useful when connecting to a MySQL instance where the external port
@@ -348,6 +350,10 @@ func getSafeTableName(baseName string, suffix string) string {
348350
// GetGhostTableName generates the name of ghost table, based on original table name
349351
// or a given table name
350352
func (this *MigrationContext) GetGhostTableName() string {
353+
if this.Revert {
354+
// When reverting the "ghost" table is the _del table from the original migration.
355+
return this.OldTableName
356+
}
351357
if this.ForceTmpTableName != "" {
352358
return getSafeTableName(this.ForceTmpTableName, "gho")
353359
} else {
@@ -364,14 +370,18 @@ func (this *MigrationContext) GetOldTableName() string {
364370
tableName = this.OriginalTableName
365371
}
366372

373+
suffix := "del"
374+
if this.Revert {
375+
suffix = "rev_del"
376+
}
367377
if this.TimestampOldTable {
368378
t := this.StartTime
369379
timestamp := fmt.Sprintf("%d%02d%02d%02d%02d%02d",
370380
t.Year(), t.Month(), t.Day(),
371381
t.Hour(), t.Minute(), t.Second())
372-
return getSafeTableName(tableName, fmt.Sprintf("%s_del", timestamp))
382+
return getSafeTableName(tableName, fmt.Sprintf("%s_%s", timestamp, suffix))
373383
}
374-
return getSafeTableName(tableName, "del")
384+
return getSafeTableName(tableName, suffix)
375385
}
376386

377387
// GetChangelogTableName generates the name of changelog table, based on original table name

go/cmd/gh-ost/main.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ func main() {
148148
flag.BoolVar(&migrationContext.Checkpoint, "checkpoint", false, "Enable migration checkpoints")
149149
flag.Int64Var(&migrationContext.CheckpointIntervalSeconds, "checkpoint-seconds", 300, "The number of seconds between checkpoints")
150150
flag.BoolVar(&migrationContext.Resume, "resume", false, "Attempt to resume migration from checkpoint")
151+
flag.BoolVar(&migrationContext.Revert, "revert", false, "Attempt to revert completed migration")
152+
flag.StringVar(&migrationContext.OldTableName, "old-table", "", "The name of the old table when using --revert, e.g. '_mytable_del'")
151153

152154
maxLoad := flag.String("max-load", "", "Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'. When status exceeds threshold, app throttles writes")
153155
criticalLoad := flag.String("critical-load", "", "Comma delimited status-name=threshold, same format as --max-load. When status exceeds threshold, app panics and quits")
@@ -206,12 +208,35 @@ func main() {
206208

207209
migrationContext.SetConnectionCharset(*charset)
208210

209-
if migrationContext.AlterStatement == "" {
211+
if migrationContext.AlterStatement == "" && !migrationContext.Revert {
210212
log.Fatal("--alter must be provided and statement must not be empty")
211213
}
212214
parser := sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
213215
migrationContext.AlterStatementOptions = parser.GetAlterStatementOptions()
214216

217+
if migrationContext.Revert {
218+
if migrationContext.Resume {
219+
log.Fatal("--revert cannot be used with --resume")
220+
}
221+
if migrationContext.OldTableName == "" {
222+
migrationContext.Log.Fatalf("--revert must be called with --old-table")
223+
}
224+
225+
// options irrelevant to revert mode
226+
if migrationContext.AlterStatement != "" {
227+
log.Warning("--alter was provided with --revert, it will be ignored")
228+
}
229+
if migrationContext.AttemptInstantDDL {
230+
log.Warning("--attempt-instant-ddl was provided with --revert, it will be ignored")
231+
}
232+
if migrationContext.IncludeTriggers {
233+
log.Warning("--include-triggers was provided with --revert, it will be ignored")
234+
}
235+
if migrationContext.DiscardForeignKeys {
236+
log.Warning("--discard-foreign-keys was provided with --revert, it will be ignored")
237+
}
238+
}
239+
215240
if migrationContext.DatabaseName == "" {
216241
if parser.HasExplicitSchema() {
217242
migrationContext.DatabaseName = parser.GetExplicitSchema()
@@ -347,7 +372,14 @@ func main() {
347372
acceptSignals(migrationContext)
348373

349374
migrator := logic.NewMigrator(migrationContext, AppVersion)
350-
if err := migrator.Migrate(); err != nil {
375+
var err error
376+
if migrationContext.Revert {
377+
err = migrator.Revert()
378+
} else {
379+
err = migrator.Migrate()
380+
}
381+
382+
if err != nil {
351383
migrator.ExecOnFailureHook()
352384
migrationContext.Log.Fatale(err)
353385
}

go/logic/applier.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -437,25 +437,20 @@ func (this *Applier) CreateCheckpointTable() error {
437437
"`gh_ost_chk_iteration` bigint",
438438
"`gh_ost_rows_copied` bigint",
439439
"`gh_ost_dml_applied` bigint",
440+
"`gh_ost_is_cutover` tinyint(1) DEFAULT '0'",
440441
}
441442
for _, col := range this.migrationContext.UniqueKey.Columns.Columns() {
442443
if col.MySQLType == "" {
443444
return fmt.Errorf("CreateCheckpoinTable: column %s has no type information. applyColumnTypes must be called", sql.EscapeName(col.Name))
444445
}
445446
minColName := sql.TruncateColumnName(col.Name, sql.MaxColumnNameLength-4) + "_min"
446447
colDef := fmt.Sprintf("%s %s", sql.EscapeName(minColName), col.MySQLType)
447-
if !col.Nullable {
448-
colDef += " NOT NULL"
449-
}
450448
colDefs = append(colDefs, colDef)
451449
}
452450

453451
for _, col := range this.migrationContext.UniqueKey.Columns.Columns() {
454452
maxColName := sql.TruncateColumnName(col.Name, sql.MaxColumnNameLength-4) + "_max"
455453
colDef := fmt.Sprintf("%s %s", sql.EscapeName(maxColName), col.MySQLType)
456-
if !col.Nullable {
457-
colDef += " NOT NULL"
458-
}
459454
colDefs = append(colDefs, colDef)
460455
}
461456

@@ -627,7 +622,7 @@ func (this *Applier) WriteCheckpoint(chk *Checkpoint) (int64, error) {
627622
if err != nil {
628623
return insertId, err
629624
}
630-
args := sqlutils.Args(chk.LastTrxCoords.String(), chk.Iteration, chk.RowsCopied, chk.DMLApplied)
625+
args := sqlutils.Args(chk.LastTrxCoords.String(), chk.Iteration, chk.RowsCopied, chk.DMLApplied, chk.IsCutover)
631626
args = append(args, uniqueKeyArgs...)
632627
res, err := this.db.Exec(query, args...)
633628
if err != nil {
@@ -637,15 +632,15 @@ func (this *Applier) WriteCheckpoint(chk *Checkpoint) (int64, error) {
637632
}
638633

639634
func (this *Applier) ReadLastCheckpoint() (*Checkpoint, error) {
640-
row := this.db.QueryRow(fmt.Sprintf(`select /* gh-ost */ * from %s.%s order by gh_ost_chk_id desc limit 1`, this.migrationContext.DatabaseName, this.migrationContext.GetCheckpointTableName()))
635+
row := this.db.QueryRow(fmt.Sprintf(`select /* gh-ost */ * from %s.%s order by gh_ost_chk_id desc limit 1`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.GetCheckpointTableName())))
641636
chk := &Checkpoint{
642637
IterationRangeMin: sql.NewColumnValues(this.migrationContext.UniqueKey.Columns.Len()),
643638
IterationRangeMax: sql.NewColumnValues(this.migrationContext.UniqueKey.Columns.Len()),
644639
}
645640

646641
var coordStr string
647642
var timestamp int64
648-
ptrs := []interface{}{&chk.Id, &timestamp, &coordStr, &chk.Iteration, &chk.RowsCopied, &chk.DMLApplied}
643+
ptrs := []interface{}{&chk.Id, &timestamp, &coordStr, &chk.Iteration, &chk.RowsCopied, &chk.DMLApplied, &chk.IsCutover}
649644
ptrs = append(ptrs, chk.IterationRangeMin.ValuesPointers...)
650645
ptrs = append(ptrs, chk.IterationRangeMax.ValuesPointers...)
651646
err := row.Scan(ptrs...)

go/logic/applier_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func (suite *ApplierTestSuite) SetupSuite() {
214214
testmysql.WithUsername(testMysqlUser),
215215
testmysql.WithPassword(testMysqlPass),
216216
testcontainers.WithWaitStrategy(wait.ForExposedPort()),
217+
testmysql.WithConfigFile("my.cnf.test"),
217218
)
218219
suite.Require().NoError(err)
219220

@@ -272,7 +273,7 @@ func (suite *ApplierTestSuite) TestInitDBConnections() {
272273
mysqlVersion, _ := strings.CutPrefix(testMysqlContainerImage, "mysql:")
273274
suite.Require().Equal(mysqlVersion, migrationContext.ApplierMySQLVersion)
274275
suite.Require().Equal(int64(28800), migrationContext.ApplierWaitTimeout)
275-
suite.Require().Equal("SYSTEM", migrationContext.ApplierTimeZone)
276+
suite.Require().Equal("+00:00", migrationContext.ApplierTimeZone)
276277

277278
suite.Require().Equal(sql.NewColumnList([]string{"id", "item_id"}), migrationContext.OriginalTableColumnsOnApplier)
278279
}
@@ -702,6 +703,7 @@ func (suite *ApplierTestSuite) TestWriteCheckpoint() {
702703
Iteration: 2,
703704
RowsCopied: 100000,
704705
DMLApplied: 200000,
706+
IsCutover: true,
705707
}
706708
id, err := applier.WriteCheckpoint(chk)
707709
suite.Require().NoError(err)
@@ -716,6 +718,7 @@ func (suite *ApplierTestSuite) TestWriteCheckpoint() {
716718
suite.Require().Equal(chk.IterationRangeMax.String(), gotChk.IterationRangeMax.String())
717719
suite.Require().Equal(chk.RowsCopied, gotChk.RowsCopied)
718720
suite.Require().Equal(chk.DMLApplied, gotChk.DMLApplied)
721+
suite.Require().Equal(chk.IsCutover, gotChk.IsCutover)
719722
}
720723

721724
func TestApplier(t *testing.T) {

go/logic/checkpoint.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ type Checkpoint struct {
2828
Iteration int64
2929
RowsCopied int64
3030
DMLApplied int64
31+
IsCutover bool
3132
}

go/logic/hooks.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) [
6969
env = append(env, fmt.Sprintf("GH_OST_HOOKS_HINT_OWNER=%s", this.migrationContext.HooksHintOwner))
7070
env = append(env, fmt.Sprintf("GH_OST_HOOKS_HINT_TOKEN=%s", this.migrationContext.HooksHintToken))
7171
env = append(env, fmt.Sprintf("GH_OST_DRY_RUN=%t", this.migrationContext.Noop))
72+
env = append(env, fmt.Sprintf("GH_OST_REVERT=%t", this.migrationContext.Revert))
7273

7374
env = append(env, extraVariables...)
7475
return env

0 commit comments

Comments
 (0)