From 3f43400e3ab5fd58eb94f64dc433e670f1a5c3ef Mon Sep 17 00:00:00 2001 From: wangzihuacool Date: Thu, 20 Oct 2022 13:34:31 +0000 Subject: [PATCH 01/30] add-rocksdb-as-transactional-engine --- go/base/context.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/go/base/context.go b/go/base/context.go index f3fe712..bdfe156 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -427,6 +427,10 @@ func (this *MigrationContext) IsTransactionalTable() bool { { return true } + case "rocksdb": + { + return true + } } return false } From df4cf7b38e5aa50037681efb588acf39cbd2ac9f Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 21 Oct 2022 17:47:39 +0200 Subject: [PATCH 02/30] Add basic test for hooks (#1179) --- go/logic/hooks.go | 11 ++-- go/logic/hooks_test.go | 113 +++++++++++++++++++++++++++++++++++++++++ go/logic/migrator.go | 13 +---- script/test | 2 +- 4 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 go/logic/hooks_test.go diff --git a/go/logic/hooks.go b/go/logic/hooks.go index 0ff296d..2543f8e 100644 --- a/go/logic/hooks.go +++ b/go/logic/hooks.go @@ -7,6 +7,7 @@ package logic import ( "fmt" + "io" "os" "os/exec" "path/filepath" @@ -34,18 +35,16 @@ const ( type HooksExecutor struct { migrationContext *base.MigrationContext + writer io.Writer } func NewHooksExecutor(migrationContext *base.MigrationContext) *HooksExecutor { return &HooksExecutor{ migrationContext: migrationContext, + writer: os.Stderr, } } -func (this *HooksExecutor) initHooks() error { - return nil -} - func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) []string { env := os.Environ() env = append(env, fmt.Sprintf("GH_OST_DATABASE_NAME=%s", this.migrationContext.DatabaseName)) @@ -76,13 +75,13 @@ func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) [ } // executeHook executes a command, and sets relevant environment variables -// combined output & error are printed to gh-ost's standard error. +// combined output & error are printed to the configured writer. func (this *HooksExecutor) executeHook(hook string, extraVariables ...string) error { cmd := exec.Command(hook) cmd.Env = this.applyEnvironmentVariables(extraVariables...) combinedOutput, err := cmd.CombinedOutput() - fmt.Fprintln(os.Stderr, string(combinedOutput)) + fmt.Fprintln(this.writer, string(combinedOutput)) return log.Errore(err) } diff --git a/go/logic/hooks_test.go b/go/logic/hooks_test.go new file mode 100644 index 0000000..3b28afe --- /dev/null +++ b/go/logic/hooks_test.go @@ -0,0 +1,113 @@ +/* + Copyright 2022 GitHub Inc. + See https://github.com/github/gh-ost/blob/master/LICENSE +*/ + +package logic + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/openark/golib/tests" + + "github.com/github/gh-ost/go/base" +) + +func TestHooksExecutorExecuteHooks(t *testing.T) { + migrationContext := base.NewMigrationContext() + migrationContext.AlterStatement = "ENGINE=InnoDB" + migrationContext.DatabaseName = "test" + migrationContext.Hostname = "test.example.com" + migrationContext.OriginalTableName = "tablename" + migrationContext.RowsDeltaEstimate = 1 + migrationContext.RowsEstimate = 122 + migrationContext.TotalRowsCopied = 123456 + migrationContext.SetETADuration(time.Minute) + migrationContext.SetProgressPct(50) + hooksExecutor := NewHooksExecutor(migrationContext) + + writeTmpHookFunc := func(testName, hookName, script string) (path string, err error) { + if path, err = os.MkdirTemp("", testName); err != nil { + return path, err + } + err = os.WriteFile(filepath.Join(path, hookName), []byte(script), 0777) + return path, err + } + + t.Run("does-not-exist", func(t *testing.T) { + migrationContext.HooksPath = "/does/not/exist" + tests.S(t).ExpectNil(hooksExecutor.executeHooks("test-hook")) + }) + + t.Run("failed", func(t *testing.T) { + var err error + if migrationContext.HooksPath, err = writeTmpHookFunc( + "TestHooksExecutorExecuteHooks-failed", + "failed-hook", + "#!/bin/sh\nexit 1", + ); err != nil { + panic(err) + } + defer os.RemoveAll(migrationContext.HooksPath) + tests.S(t).ExpectNotNil(hooksExecutor.executeHooks("failed-hook")) + }) + + t.Run("success", func(t *testing.T) { + var err error + if migrationContext.HooksPath, err = writeTmpHookFunc( + "TestHooksExecutorExecuteHooks-success", + "success-hook", + "#!/bin/sh\nenv", + ); err != nil { + panic(err) + } + defer os.RemoveAll(migrationContext.HooksPath) + + var buf bytes.Buffer + hooksExecutor.writer = &buf + tests.S(t).ExpectNil(hooksExecutor.executeHooks("success-hook", "TEST="+t.Name())) + + scanner := bufio.NewScanner(&buf) + for scanner.Scan() { + split := strings.SplitN(scanner.Text(), "=", 2) + switch split[0] { + case "GH_OST_COPIED_ROWS": + copiedRows, _ := strconv.ParseInt(split[1], 10, 64) + tests.S(t).ExpectEquals(copiedRows, migrationContext.TotalRowsCopied) + case "GH_OST_DATABASE_NAME": + tests.S(t).ExpectEquals(split[1], migrationContext.DatabaseName) + case "GH_OST_DDL": + tests.S(t).ExpectEquals(split[1], migrationContext.AlterStatement) + case "GH_OST_DRY_RUN": + tests.S(t).ExpectEquals(split[1], "false") + case "GH_OST_ESTIMATED_ROWS": + estimatedRows, _ := strconv.ParseInt(split[1], 10, 64) + tests.S(t).ExpectEquals(estimatedRows, int64(123)) + case "GH_OST_ETA_SECONDS": + etaSeconds, _ := strconv.ParseInt(split[1], 10, 64) + tests.S(t).ExpectEquals(etaSeconds, int64(60)) + case "GH_OST_EXECUTING_HOST": + tests.S(t).ExpectEquals(split[1], migrationContext.Hostname) + case "GH_OST_GHOST_TABLE_NAME": + tests.S(t).ExpectEquals(split[1], fmt.Sprintf("_%s_gho", migrationContext.OriginalTableName)) + case "GH_OST_OLD_TABLE_NAME": + tests.S(t).ExpectEquals(split[1], fmt.Sprintf("_%s_del", migrationContext.OriginalTableName)) + case "GH_OST_PROGRESS": + progress, _ := strconv.ParseFloat(split[1], 64) + tests.S(t).ExpectEquals(progress, 50.0) + case "GH_OST_TABLE_NAME": + tests.S(t).ExpectEquals(split[1], migrationContext.OriginalTableName) + case "TEST": + tests.S(t).ExpectEquals(split[1], t.Name()) + } + } + }) +} diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 865814d..ccaf160 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -98,6 +98,7 @@ type Migrator struct { func NewMigrator(context *base.MigrationContext, appVersion string) *Migrator { migrator := &Migrator{ appVersion: appVersion, + hooksExecutor: NewHooksExecutor(context), migrationContext: context, parser: sql.NewAlterTableParser(), ghostTableMigrated: make(chan bool), @@ -113,15 +114,6 @@ func NewMigrator(context *base.MigrationContext, appVersion string) *Migrator { return migrator } -// initiateHooksExecutor -func (this *Migrator) initiateHooksExecutor() (err error) { - this.hooksExecutor = NewHooksExecutor(this.migrationContext) - if err := this.hooksExecutor.initHooks(); err != nil { - return err - } - return nil -} - // sleepWhileTrue sleeps indefinitely until the given function returns 'false' // (or fails with error) func (this *Migrator) sleepWhileTrue(operation func() (bool, error)) error { @@ -342,9 +334,6 @@ func (this *Migrator) Migrate() (err error) { go this.listenOnPanicAbort() - if err := this.initiateHooksExecutor(); err != nil { - return err - } if err := this.hooksExecutor.onStartup(); err != nil { return err } diff --git a/script/test b/script/test index 7e757b5..5c32b37 100755 --- a/script/test +++ b/script/test @@ -14,4 +14,4 @@ script/build cd .gopath/src/github.com/github/gh-ost echo "Running unit tests" -go test ./go/... +go test -v -covermode=atomic ./go/... From 9b3fa793ac2500a08dda89a9b8ee4497167bccbf Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 21 Oct 2022 18:02:06 +0200 Subject: [PATCH 03/30] Enable more `golangci-lint` linters (#1181) --- .golangci.yml | 6 ++++++ go/base/context.go | 4 ++-- go/logic/applier.go | 2 +- go/logic/inspect.go | 7 +++---- go/logic/migrator.go | 10 ++++------ go/logic/throttler.go | 2 ++ go/sql/parser.go | 10 ++++------ go/sql/parser_test.go | 14 +++++++------- 8 files changed, 29 insertions(+), 26 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4621e5c..e4ee4ab 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,14 +4,19 @@ linters: disable: - errcheck enable: + - bodyclose + - containedctx - contextcheck + - dogsled - durationcheck - errname + - errorlint - execinquery - gofmt - ifshort - misspell - nilerr + - nilnil - noctx - nolintlint - nosprintfhostport @@ -19,6 +24,7 @@ linters: - rowserrcheck - sqlclosecheck - unconvert + - unparam - unused - wastedassign - whitespace diff --git a/go/base/context.go b/go/base/context.go index f3fe712..270b7a0 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -858,7 +858,7 @@ func (this *MigrationContext) ReadConfigFile() error { if cfg.Section("osc").HasKey("chunk_size") { this.config.Osc.Chunk_Size, err = cfg.Section("osc").Key("chunk_size").Int64() if err != nil { - return fmt.Errorf("Unable to read osc chunk size: %s", err.Error()) + return fmt.Errorf("Unable to read osc chunk size: %w", err) } } @@ -873,7 +873,7 @@ func (this *MigrationContext) ReadConfigFile() error { if cfg.Section("osc").HasKey("max_lag_millis") { this.config.Osc.Max_Lag_Millis, err = cfg.Section("osc").Key("max_lag_millis").Int64() if err != nil { - return fmt.Errorf("Unable to read max lag millis: %s", err.Error()) + return fmt.Errorf("Unable to read max lag millis: %w", err) } } diff --git a/go/logic/applier.go b/go/logic/applier.go index 89e1995..50fd9bd 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -1134,7 +1134,7 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent)) } result, err := tx.Exec(buildResult.query, buildResult.args...) if err != nil { - err = fmt.Errorf("%s; query=%s; args=%+v", err.Error(), buildResult.query, buildResult.args) + err = fmt.Errorf("%w; query=%s; args=%+v", err, buildResult.query, buildResult.args) return rollback(err) } diff --git a/go/logic/inspect.go b/go/logic/inspect.go index 4df80fe..3ece8ab 100644 --- a/go/logic/inspect.go +++ b/go/logic/inspect.go @@ -8,6 +8,7 @@ package logic import ( "context" gosql "database/sql" + "errors" "fmt" "reflect" "strings" @@ -554,13 +555,11 @@ func (this *Inspector) CountTableRows(ctx context.Context) error { query := fmt.Sprintf(`select /* gh-ost */ count(*) as count_rows from %s.%s`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) var rowsEstimate int64 if err := conn.QueryRowContext(ctx, query).Scan(&rowsEstimate); err != nil { - switch err { - case context.Canceled, context.DeadlineExceeded: + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { this.migrationContext.Log.Infof("exact row count cancelled (%s), likely because I'm about to cut over. I'm going to kill that query.", ctx.Err()) return mysql.Kill(this.db, connectionID) - default: - return err } + return err } // row count query finished. nil out the cancel func, so the main migration thread diff --git a/go/logic/migrator.go b/go/logic/migrator.go index ccaf160..b443d69 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -391,9 +391,9 @@ func (this *Migrator) Migrate() (err error) { if err := this.applier.ReadMigrationRangeValues(); err != nil { return err } - if err := this.initiateThrottler(); err != nil { - return err - } + + this.initiateThrottler() + if err := this.hooksExecutor.onBeforeRowCopy(); err != nil { return err } @@ -1096,7 +1096,7 @@ func (this *Migrator) addDMLEventsListener() error { } // initiateThrottler kicks in the throttling collection and the throttling checks. -func (this *Migrator) initiateThrottler() error { +func (this *Migrator) initiateThrottler() { this.throttler = NewThrottler(this.migrationContext, this.applier, this.inspector, this.appVersion) go this.throttler.initiateThrottlerCollection(this.firstThrottlingCollected) @@ -1106,8 +1106,6 @@ func (this *Migrator) initiateThrottler() error { <-this.firstThrottlingCollected // other, general metrics this.migrationContext.Log.Infof("First throttle metrics collected") go this.throttler.initiateThrottlerChecks() - - return nil } func (this *Migrator) initiateApplier() error { diff --git a/go/logic/throttler.go b/go/logic/throttler.go index 1e7bc97..9c8dcfc 100644 --- a/go/logic/throttler.go +++ b/go/logic/throttler.go @@ -308,6 +308,8 @@ func (this *Throttler) collectThrottleHTTPStatus(firstThrottlingCollected chan<- if err != nil { return false, err } + defer resp.Body.Close() + atomic.StoreInt64(&this.migrationContext.ThrottleHTTPStatusCode, int64(resp.StatusCode)) return false, nil } diff --git a/go/sql/parser.go b/go/sql/parser.go index a72af33..2ddc60f 100644 --- a/go/sql/parser.go +++ b/go/sql/parser.go @@ -62,7 +62,7 @@ func NewParserFromAlterStatement(alterStatement string) *AlterTableParser { return parser } -func (this *AlterTableParser) tokenizeAlterStatement(alterStatement string) (tokens []string, err error) { +func (this *AlterTableParser) tokenizeAlterStatement(alterStatement string) (tokens []string) { terminatingQuote := rune(0) f := func(c rune) bool { switch { @@ -86,7 +86,7 @@ func (this *AlterTableParser) tokenizeAlterStatement(alterStatement string) (tok for i := range tokens { tokens[i] = strings.TrimSpace(tokens[i]) } - return tokens, nil + return tokens } func (this *AlterTableParser) sanitizeQuotesFromAlterStatement(alterStatement string) (strippedStatement string) { @@ -95,7 +95,7 @@ func (this *AlterTableParser) sanitizeQuotesFromAlterStatement(alterStatement st return strippedStatement } -func (this *AlterTableParser) parseAlterToken(alterToken string) (err error) { +func (this *AlterTableParser) parseAlterToken(alterToken string) { { // rename allStringSubmatch := renameColumnRegexp.FindAllStringSubmatch(alterToken, -1) @@ -131,7 +131,6 @@ func (this *AlterTableParser) parseAlterToken(alterToken string) (err error) { this.isAutoIncrementDefined = true } } - return nil } func (this *AlterTableParser) ParseAlterStatement(alterStatement string) (err error) { @@ -151,8 +150,7 @@ func (this *AlterTableParser) ParseAlterStatement(alterStatement string) (err er break } } - alterTokens, _ := this.tokenizeAlterStatement(this.alterStatementOptions) - for _, alterToken := range alterTokens { + for _, alterToken := range this.tokenizeAlterStatement(this.alterStatementOptions) { alterToken = this.sanitizeQuotesFromAlterStatement(alterToken) this.parseAlterToken(alterToken) this.alterTokens = append(this.alterTokens, alterToken) diff --git a/go/sql/parser_test.go b/go/sql/parser_test.go index 79a9b7b..df92842 100644 --- a/go/sql/parser_test.go +++ b/go/sql/parser_test.go @@ -99,37 +99,37 @@ func TestTokenizeAlterStatement(t *testing.T) { parser := NewAlterTableParser() { alterStatement := "add column t int" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int"})) } { alterStatement := "add column t int, change column i int" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int"})) } { alterStatement := "add column t int, change column i int 'some comment'" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int 'some comment'"})) } { alterStatement := "add column t int, change column i int 'some comment, with comma'" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "change column i int 'some comment, with comma'"})) } { alterStatement := "add column t int, add column d decimal(10,2)" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "add column d decimal(10,2)"})) } { alterStatement := "add column t int, add column e enum('a','b','c')" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int", "add column e enum('a','b','c')"})) } { alterStatement := "add column t int(11), add column e enum('a','b','c')" - tokens, _ := parser.tokenizeAlterStatement(alterStatement) + tokens := parser.tokenizeAlterStatement(alterStatement) test.S(t).ExpectTrue(reflect.DeepEqual(tokens, []string{"add column t int(11)", "add column e enum('a','b','c')"})) } } From 2793e2b6b317465e06a9e9a99f4af52ebd64a304 Mon Sep 17 00:00:00 2001 From: Hasan Mshawrab <63023909+hasanMshawrab@users.noreply.github.com> Date: Wed, 26 Oct 2022 10:28:27 +0300 Subject: [PATCH 04/30] Fix: Change table name table name is 'tbl' not 'tble' --- doc/shared-key.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/shared-key.md b/doc/shared-key.md index c7f24cc..3dfa39b 100644 --- a/doc/shared-key.md +++ b/doc/shared-key.md @@ -29,7 +29,7 @@ CREATE TABLE tbl ( (This is also the definition of the _ghost_ table, except that that table would be called `_tbl_gho`). -In this migration, the _before_ and _after_ versions contain the same unique not-null key (the PRIMARY KEY). To run this migration, `gh-ost` would iterate through the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` in primary key order, while also applying the binlog event writes from `tble` onto `_tbl_gho`. +In this migration, the _before_ and _after_ versions contain the same unique not-null key (the PRIMARY KEY). To run this migration, `gh-ost` would iterate through the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` in primary key order, while also applying the binlog event writes from `tbl` onto `_tbl_gho`. The applying of the binlog events is what requires the shared unique key. For example, an `UPDATE` statement to `tbl` translates to a `REPLACE` statement which `gh-ost` applies to `_tbl_gho`. A `REPLACE` statement expects to insert or replace an existing row based on its row's values and the table's unique key constraints. In particular, if inserting that row would result in a unique key violation (e.g., a row with that primary key already exists), it would _replace_ that existing row with the new values. From 515aa72d3d9b756e454b0168b4e57bc599b45e36 Mon Sep 17 00:00:00 2001 From: Nicholas Calugar Date: Thu, 27 Oct 2022 11:42:12 -0700 Subject: [PATCH 05/30] Print status to migration context logger --- go/logic/migrator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/logic/migrator.go b/go/logic/migrator.go index b443d69..22b7c9f 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -1036,6 +1036,7 @@ func (this *Migrator) printStatus(rule PrintStatusRule, writers ...io.Writer) { ) w := io.MultiWriter(writers...) fmt.Fprintln(w, status) + this.migrationContext.Log.Infof(status) hooksStatusIntervalSec := this.migrationContext.HooksStatusIntervalSec if hooksStatusIntervalSec > 0 && elapsedSeconds%hooksStatusIntervalSec == 0 { From 1be6a4c0823d3ed22f58ff1ff77bc34eff7797a0 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Wed, 9 Nov 2022 20:11:49 -0700 Subject: [PATCH 06/30] Attempt instant DDL if supported --- go/base/context.go | 1 + go/cmd/gh-ost/main.go | 2 ++ go/logic/applier.go | 48 +++++++++++++++++++++++++++++++++++++++++++ go/logic/migrator.go | 25 ++++++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/go/base/context.go b/go/base/context.go index 270b7a0..6032a93 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -101,6 +101,7 @@ type MigrationContext struct { AliyunRDS bool GoogleCloudPlatform bool AzureMySQL bool + AttemptInstantDDL bool config ContextConfig configMutex *sync.Mutex diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index b99e70b..660c492 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -67,6 +67,8 @@ func main() { flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)") flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)") flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)") + flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", true, "Attempt to use instant DDL for this migration first.") + flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)") flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy") flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica") diff --git a/go/logic/applier.go b/go/logic/applier.go index 50fd9bd..b9313a3 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -188,6 +188,54 @@ func (this *Applier) ValidateOrDropExistingTables() error { return nil } +// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others.) +// to apply the ALTER statement immediately. If it errors, the original +// gh-ost algorithm can be used. However, if it's successful -- a lot +// of time can potentially be saved. Instant operations include: +// - Adding a column +// - Dropping a column +// - Dropping an index +// - Extending a varchar column +// It is safer to attempt the change than try and parse the DDL, since +// there might be specifics about the table which make it not possible to apply instantly. +func (this *Applier) AttemptInstantDDL() error { + + query := fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`, + sql.EscapeName(this.migrationContext.DatabaseName), + sql.EscapeName(this.migrationContext.OriginalTableName), + this.migrationContext.AlterStatementOptions, + ) + this.migrationContext.Log.Infof("INSTANT DDL Query is: %s", query) + + err := func() error { + tx, err := this.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone) + sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery()) + + if _, err := tx.Exec(sessionQuery); err != nil { + return err + } + if _, err := tx.Exec(query); err != nil { + this.migrationContext.Log.Infof("INSTANT DDL failed: %s", err) + return err + } + if err := tx.Commit(); err != nil { + // Neither SET SESSION nor ALTER are really transactional, so strictly speaking + // there's no need to commit; but let's do this the legit way anyway. + return err + } + return nil + }() + + return err + +} + // CreateGhostTable creates the ghost table on the applier host func (this *Applier) CreateGhostTable() error { query := fmt.Sprintf(`create /* gh-ost */ table %s.%s like %s.%s`, diff --git a/go/logic/migrator.go b/go/logic/migrator.go index b443d69..40415d0 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -351,12 +351,26 @@ func (this *Migrator) Migrate() (err error) { if err := this.initiateInspector(); err != nil { return err } + // In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly. + // As just a metadata change. We can't detect this unless we attempt the statement + // (i.e. there is no explain for DDL). + if this.migrationContext.AttemptInstantDDL { + this.migrationContext.Log.Infof("Attempting to execute ALTER TABLE as INSTANT DDL") + if err := this.attemptInstantDDL(); err == nil { + this.migrationContext.Log.Infof("Success! Table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) + return nil + } else { + this.migrationContext.Log.Infof("INSTANT DDL failed, will proceed with original algorithm: %s", err) + } + } + if err := this.initiateStreaming(); err != nil { return err } if err := this.initiateApplier(); err != nil { return err } + if err := this.createFlagFiles(); err != nil { return err } @@ -734,6 +748,17 @@ func (this *Migrator) initiateServer() (err error) { return nil } +// attemptInstantDDL tries to apply the DDL statement directly to the table +// using a ALGORITHM=INSTANT assertion. If this fails, it will return an error, +// in which case the original algorithm should be used. +func (this *Migrator) attemptInstantDDL() (err error) { + this.applier = NewApplier(this.migrationContext) + if err := this.applier.InitDBConnections(); err != nil { + return err + } + return this.applier.AttemptInstantDDL() +} + // initiateInspector connects, validates and inspects the "inspector" server. // The "inspector" server is typically a replica; it is where we issue some // queries such as: From 05f32ebf297456ee759ced15aaa124acb6e7d207 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Thu, 10 Nov 2022 09:30:13 -0700 Subject: [PATCH 07/30] minor cleanup --- go/logic/applier.go | 32 +++----------------------------- go/logic/migrator.go | 1 - 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/go/logic/applier.go b/go/logic/applier.go index b9313a3..abda2b0 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -199,41 +199,15 @@ func (this *Applier) ValidateOrDropExistingTables() error { // It is safer to attempt the change than try and parse the DDL, since // there might be specifics about the table which make it not possible to apply instantly. func (this *Applier) AttemptInstantDDL() error { - query := fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName), this.migrationContext.AlterStatementOptions, ) - this.migrationContext.Log.Infof("INSTANT DDL Query is: %s", query) - - err := func() error { - tx, err := this.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone) - sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery()) - - if _, err := tx.Exec(sessionQuery); err != nil { - return err - } - if _, err := tx.Exec(query); err != nil { - this.migrationContext.Log.Infof("INSTANT DDL failed: %s", err) - return err - } - if err := tx.Commit(); err != nil { - // Neither SET SESSION nor ALTER are really transactional, so strictly speaking - // there's no need to commit; but let's do this the legit way anyway. - return err - } - return nil - }() - + this.migrationContext.Log.Infof("INSTANT DDL query is: %s", query) + // We don't need a trx, because for instant DDL the SQL mode doesn't matter. + _, err := this.db.Exec(query) return err - } // CreateGhostTable creates the ghost table on the applier host diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 40415d0..25989f8 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -370,7 +370,6 @@ func (this *Migrator) Migrate() (err error) { if err := this.initiateApplier(); err != nil { return err } - if err := this.createFlagFiles(); err != nil { return err } From 9cbb42b92492757639c704e5c601ec5cac602909 Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Mon, 14 Nov 2022 15:00:50 +0000 Subject: [PATCH 08/30] fix CI tests to ubuntu-20.04 because ubuntu-22.04 (current -latest) doesn't support MySQL 5.7 --- .github/workflows/ci.yml | 2 +- .github/workflows/replica-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d5a07b..0b83bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/replica-tests.yml b/.github/workflows/replica-tests.yml index e28c2bc..0a82f00 100644 --- a/.github/workflows/replica-tests.yml +++ b/.github/workflows/replica-tests.yml @@ -5,7 +5,7 @@ on: [pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: version: [mysql-5.7.25,mysql-8.0.16] From 75a346be93a7ab3b9097fe05ef2451adeadccbdd Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Mon, 14 Nov 2022 11:29:26 -0700 Subject: [PATCH 09/30] Add tests, incorporate feedback --- doc/command-line-flags.md | 16 +++++++++++ go/cmd/gh-ost/main.go | 2 +- go/logic/applier.go | 33 ++++++++++++++--------- go/logic/applier_test.go | 14 ++++++++++ go/logic/migrator.go | 9 +++---- localtests/attempt-instant-ddl/create.sql | 13 +++++++++ localtests/attempt-instant-ddl/extra_args | 1 + 7 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 localtests/attempt-instant-ddl/create.sql create mode 100644 localtests/attempt-instant-ddl/extra_args diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index dc481d0..b496b04 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -45,6 +45,22 @@ If you happen to _know_ your servers use RBR (Row Based Replication, i.e. `binlo Skipping this step means `gh-ost` would not need the `SUPER` privilege in order to operate. You may want to use this on Amazon RDS. +### attempt-instant-ddl + +MySQL 8.0 support "instant DDL" for some operations. If an alter statement can be completed with instant DDL, only a metadata change is required internally, so MySQL will return _instantly_ (only requiring a metadata lock to complete). Instant operations include: + +- Adding a column +- Dropping a column +- Dropping an index +- Extending a varchar column +- Adding a virtual generated column + +It is not reliable to parse the `ALTER` statement to determine if it is instant or not. This is because the table might be in an older row format, or have some other incompatibility that is difficult to identify. + +The risks of attempting to instant DDL are relatively minor: Gh-ost may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions. + +gh-ost will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful. + ### conf `--conf=/path/to/my.cnf`: file where credentials are specified. Should be in (or contain) the following format: diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 660c492..c00b206 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -67,7 +67,7 @@ func main() { flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)") flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)") flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)") - flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", true, "Attempt to use instant DDL for this migration first.") + flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", false, "Attempt to use instant DDL for this migration first") flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)") flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy") diff --git a/go/logic/applier.go b/go/logic/applier.go index abda2b0..ad6368e 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -135,6 +135,16 @@ func (this *Applier) generateSqlModeQuery() string { return fmt.Sprintf("sql_mode = %s", sqlModeQuery) } +// generateInstantDDLQuery returns the SQL for this ALTER operation +// with an INSTANT assertion (requires MySQL 8.0+) +func (this *Applier) generateInstantDDLQuery() string { + return fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`, + sql.EscapeName(this.migrationContext.DatabaseName), + sql.EscapeName(this.migrationContext.OriginalTableName), + this.migrationContext.AlterStatementOptions, + ) +} + // readTableColumns reads table columns on applier func (this *Applier) readTableColumns() (err error) { this.migrationContext.Log.Infof("Examining table structure on applier") @@ -188,22 +198,21 @@ func (this *Applier) ValidateOrDropExistingTables() error { return nil } -// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others.) -// to apply the ALTER statement immediately. If it errors, the original -// gh-ost algorithm can be used. However, if it's successful -- a lot -// of time can potentially be saved. Instant operations include: +// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others). +// If successful, the operation is only a meta-data change so a lot of time is saved! +// The risk of attempting to instant DDL when not supported is that a metadata lock may be acquired. +// This is minor, since gh-ost will eventually require a metadata lock anyway, but at the cut-over stage. +// Instant operations include: // - Adding a column // - Dropping a column // - Dropping an index -// - Extending a varchar column -// It is safer to attempt the change than try and parse the DDL, since -// there might be specifics about the table which make it not possible to apply instantly. +// - Extending a VARCHAR column +// - Adding a virtual generated column +// It is not reliable to parse the `alter` statement to determine if it is instant or not. +// This is because the table might be in an older row format, or have some other incompatibility +// that is difficult to identify. func (this *Applier) AttemptInstantDDL() error { - query := fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`, - sql.EscapeName(this.migrationContext.DatabaseName), - sql.EscapeName(this.migrationContext.OriginalTableName), - this.migrationContext.AlterStatementOptions, - ) + query := this.generateInstantDDLQuery() this.migrationContext.Log.Infof("INSTANT DDL query is: %s", query) // We don't need a trx, because for instant DDL the SQL mode doesn't matter. _, err := this.db.Exec(query) diff --git a/go/logic/applier_test.go b/go/logic/applier_test.go index a2c1414..44d1fc7 100644 --- a/go/logic/applier_test.go +++ b/go/logic/applier_test.go @@ -170,3 +170,17 @@ func TestApplierBuildDMLEventQuery(t *testing.T) { test.S(t).ExpectEquals(res[0].args[3], 42) }) } + +func TestApplierInstantDDL(t *testing.T) { + migrationContext := base.NewMigrationContext() + migrationContext.DatabaseName = "test" + migrationContext.OriginalTableName = "mytable" + migrationContext.AlterStatementOptions = "ADD INDEX (foo)" + applier := NewApplier(migrationContext) + + t.Run("instantDDLstmt", func(t *testing.T) { + stmt := applier.generateInstantDDLQuery() + test.S(t).ExpectEquals(stmt, "ALTER /* gh-ost */ TABLE `test`.`mytable` ADD INDEX (foo), ALGORITHM=INSTANT") + }) + +} diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 25989f8..13cbdee 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -352,15 +352,14 @@ func (this *Migrator) Migrate() (err error) { return err } // In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly. - // As just a metadata change. We can't detect this unless we attempt the statement - // (i.e. there is no explain for DDL). + // Attempt to do this if AttemptInstantDDL is set. if this.migrationContext.AttemptInstantDDL { - this.migrationContext.Log.Infof("Attempting to execute ALTER TABLE as INSTANT DDL") + this.migrationContext.Log.Infof("Attempting to execute alter with ALGORITHM=INSTANT") if err := this.attemptInstantDDL(); err == nil { - this.migrationContext.Log.Infof("Success! Table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) + this.migrationContext.Log.Infof("Success! table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) return nil } else { - this.migrationContext.Log.Infof("INSTANT DDL failed, will proceed with original algorithm: %s", err) + this.migrationContext.Log.Infof("ALGORITHM=INSTANT failed, proceeding with original algorithm: %s", err) } } diff --git a/localtests/attempt-instant-ddl/create.sql b/localtests/attempt-instant-ddl/create.sql new file mode 100644 index 0000000..9371238 --- /dev/null +++ b/localtests/attempt-instant-ddl/create.sql @@ -0,0 +1,13 @@ +drop table if exists gh_ost_test; +create table gh_ost_test ( + id int auto_increment, + i int not null, + color varchar(32), + primary key(id) +) auto_increment=1; + +drop event if exists gh_ost_test; + +insert into gh_ost_test values (null, 11, 'red'); +insert into gh_ost_test values (null, 13, 'green'); +insert into gh_ost_test values (null, 17, 'blue'); diff --git a/localtests/attempt-instant-ddl/extra_args b/localtests/attempt-instant-ddl/extra_args new file mode 100644 index 0000000..70c8a52 --- /dev/null +++ b/localtests/attempt-instant-ddl/extra_args @@ -0,0 +1 @@ +--attempt-instant-ddl From b06c1cd498b31f601abe857b0b3bd5a4737aa586 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Mon, 14 Nov 2022 12:30:38 -0700 Subject: [PATCH 10/30] Improve docs --- doc/command-line-flags.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index b496b04..509fa67 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -47,7 +47,7 @@ You may want to use this on Amazon RDS. ### attempt-instant-ddl -MySQL 8.0 support "instant DDL" for some operations. If an alter statement can be completed with instant DDL, only a metadata change is required internally, so MySQL will return _instantly_ (only requiring a metadata lock to complete). Instant operations include: +MySQL 8.0 supports "instant DDL" for some operations. If an alter statement can be completed with instant DDL, only a metadata change is required internally. Instant operations include: - Adding a column - Dropping a column @@ -57,9 +57,9 @@ MySQL 8.0 support "instant DDL" for some operations. If an alter statement can b It is not reliable to parse the `ALTER` statement to determine if it is instant or not. This is because the table might be in an older row format, or have some other incompatibility that is difficult to identify. -The risks of attempting to instant DDL are relatively minor: Gh-ost may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions. +The risks of attempting instant DDL are relatively minor: `gh-ost` may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions. -gh-ost will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful. +`gh-ost` will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful. ### conf From 3f3a10a2133ae7318536f260bcdbd42ad700e2b0 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Mon, 14 Nov 2022 14:47:00 -0700 Subject: [PATCH 11/30] Address PR feedback --- go/logic/applier_test.go | 1 - go/logic/migrator.go | 23 +++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/go/logic/applier_test.go b/go/logic/applier_test.go index 44d1fc7..a356351 100644 --- a/go/logic/applier_test.go +++ b/go/logic/applier_test.go @@ -182,5 +182,4 @@ func TestApplierInstantDDL(t *testing.T) { stmt := applier.generateInstantDDLQuery() test.S(t).ExpectEquals(stmt, "ALTER /* gh-ost */ TABLE `test`.`mytable` ADD INDEX (foo), ALGORITHM=INSTANT") }) - } diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 0dcf53f..31e21cc 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -351,6 +351,15 @@ func (this *Migrator) Migrate() (err error) { if err := this.initiateInspector(); err != nil { return err } + if err := this.initiateStreaming(); err != nil { + return err + } + if err := this.initiateApplier(); err != nil { + return err + } + if err := this.createFlagFiles(); err != nil { + return err + } // In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly. // Attempt to do this if AttemptInstantDDL is set. if this.migrationContext.AttemptInstantDDL { @@ -363,16 +372,6 @@ func (this *Migrator) Migrate() (err error) { } } - if err := this.initiateStreaming(); err != nil { - return err - } - if err := this.initiateApplier(); err != nil { - return err - } - if err := this.createFlagFiles(); err != nil { - return err - } - initialLag, _ := this.inspector.getReplicationLag() this.migrationContext.Log.Infof("Waiting for ghost table to be migrated. Current lag is %+v", initialLag) <-this.ghostTableMigrated @@ -750,10 +749,6 @@ func (this *Migrator) initiateServer() (err error) { // using a ALGORITHM=INSTANT assertion. If this fails, it will return an error, // in which case the original algorithm should be used. func (this *Migrator) attemptInstantDDL() (err error) { - this.applier = NewApplier(this.migrationContext) - if err := this.applier.InitDBConnections(); err != nil { - return err - } return this.applier.AttemptInstantDDL() } From 1b22f784e3858b21767c78c6f74547db23e90266 Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Tue, 15 Nov 2022 15:06:19 +0000 Subject: [PATCH 12/30] temp commit to investigate datetime-with-zero test failure --- script/cibuild-gh-ost-replica-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index c4dbfd2..bc8b788 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -61,7 +61,7 @@ test_mysql_version() { gh-ost-test-mysql-master -uroot -e "grant all on *.* to 'gh-ost'@'%'" echo "### Running gh-ost tests for $mysql_version" - ./localtests/test.sh -b bin/gh-ost + bash ./localtests/test.sh -b bin/gh-ost datetime-with-zero find sandboxes -name "stop_all" | bash } From 512751968feb38cea5995c908540d9fe5f583766 Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:01:54 +0000 Subject: [PATCH 13/30] more testing --- script/cibuild-gh-ost-replica-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index bc8b788..0c37b27 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -61,7 +61,7 @@ test_mysql_version() { gh-ost-test-mysql-master -uroot -e "grant all on *.* to 'gh-ost'@'%'" echo "### Running gh-ost tests for $mysql_version" - bash ./localtests/test.sh -b bin/gh-ost datetime-with-zero + bash -x ./localtests/test.sh -b bin/gh-ost find sandboxes -name "stop_all" | bash } From 74fb8e80b2a0f848a265b34f04ca8dbfb265743f Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Tue, 15 Nov 2022 12:05:27 -0700 Subject: [PATCH 14/30] Update go/logic/migrator.go Co-authored-by: dm-2 <45519614+dm-2@users.noreply.github.com> --- go/logic/migrator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 31e21cc..0f4ebba 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -368,7 +368,7 @@ func (this *Migrator) Migrate() (err error) { this.migrationContext.Log.Infof("Success! table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) return nil } else { - this.migrationContext.Log.Infof("ALGORITHM=INSTANT failed, proceeding with original algorithm: %s", err) + this.migrationContext.Log.Infof("ALGORITHM=INSTANT not supported for this operation, proceeding with original algorithm: %s", err) } } From 5283b46ec23c494b4b4256b2e5a43d0b9e4f8974 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Tue, 15 Nov 2022 12:06:11 -0700 Subject: [PATCH 15/30] Make it clear in docs it is disabled by default but safe. --- doc/command-line-flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index 509fa67..56bc642 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -57,7 +57,7 @@ MySQL 8.0 supports "instant DDL" for some operations. If an alter statement can It is not reliable to parse the `ALTER` statement to determine if it is instant or not. This is because the table might be in an older row format, or have some other incompatibility that is difficult to identify. -The risks of attempting instant DDL are relatively minor: `gh-ost` may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions. +`--attempt-instant-ddl` is disabled by default, but the risks of enabling it are relatively minor: `gh-ost` may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions. `gh-ost` will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful. From 59c1a24774482723f100f1213bb16b16497b40f6 Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Tue, 15 Nov 2022 15:40:21 -0700 Subject: [PATCH 16/30] remove useless func per review --- go/logic/migrator.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 0f4ebba..a102188 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -364,7 +364,7 @@ func (this *Migrator) Migrate() (err error) { // Attempt to do this if AttemptInstantDDL is set. if this.migrationContext.AttemptInstantDDL { this.migrationContext.Log.Infof("Attempting to execute alter with ALGORITHM=INSTANT") - if err := this.attemptInstantDDL(); err == nil { + if err := this.applier.AttemptInstantDDL(); err == nil { this.migrationContext.Log.Infof("Success! table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) return nil } else { @@ -745,13 +745,6 @@ func (this *Migrator) initiateServer() (err error) { return nil } -// attemptInstantDDL tries to apply the DDL statement directly to the table -// using a ALGORITHM=INSTANT assertion. If this fails, it will return an error, -// in which case the original algorithm should be used. -func (this *Migrator) attemptInstantDDL() (err error) { - return this.applier.AttemptInstantDDL() -} - // initiateInspector connects, validates and inspects the "inspector" server. // The "inspector" server is typically a replica; it is where we issue some // queries such as: From cd65c7e9ada627e0fec413ff1ac6a8e7822ac88c Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:11:44 +0000 Subject: [PATCH 17/30] add extra debugging output --- localtests/test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/localtests/test.sh b/localtests/test.sh index f66c813..a3c8fa4 100755 --- a/localtests/test.sh +++ b/localtests/test.sh @@ -116,7 +116,8 @@ test_single() { gh-ost-test-mysql-replica --default-character-set=utf8mb4 test -e "set @@global.sql_mode='$(cat $tests_path/$test_name/sql_mode)'" fi - gh-ost-test-mysql-master --default-character-set=utf8mb4 test < $tests_path/$test_name/create.sql + cat $tests_path/$test_name/create.sql + bash -x gh-ost-test-mysql-master --default-character-set=utf8mb4 test < $tests_path/$test_name/create.sql extra_args="" if [ -f $tests_path/$test_name/extra_args ] ; then From 7cebc16c6fc56e168f9732a152afa54f9d71a6ae Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:32:34 +0000 Subject: [PATCH 18/30] debugging --- localtests/test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/localtests/test.sh b/localtests/test.sh index a3c8fa4..b953861 100755 --- a/localtests/test.sh +++ b/localtests/test.sh @@ -118,6 +118,7 @@ test_single() { cat $tests_path/$test_name/create.sql bash -x gh-ost-test-mysql-master --default-character-set=utf8mb4 test < $tests_path/$test_name/create.sql + echo $? extra_args="" if [ -f $tests_path/$test_name/extra_args ] ; then From 756f3d30e8d4252f30f5e7a8721a158e789355d6 Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:44:14 +0000 Subject: [PATCH 19/30] add error detection for test setup, sort tests to make it easier to track progress --- localtests/test.sh | 14 ++++++++++---- script/cibuild-gh-ost-replica-tests | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/localtests/test.sh b/localtests/test.sh index b953861..5ca06ad 100755 --- a/localtests/test.sh +++ b/localtests/test.sh @@ -116,9 +116,15 @@ test_single() { gh-ost-test-mysql-replica --default-character-set=utf8mb4 test -e "set @@global.sql_mode='$(cat $tests_path/$test_name/sql_mode)'" fi - cat $tests_path/$test_name/create.sql - bash -x gh-ost-test-mysql-master --default-character-set=utf8mb4 test < $tests_path/$test_name/create.sql - echo $? + gh-ost-test-mysql-master --default-character-set=utf8mb4 test < $tests_path/$test_name/create.sql + test_create_result=$? + + if [ $test_create_result -ne 0 ] ; then + echo + echo "ERROR $test_name create failure. cat $tests_path/$test_name/create.sql:" + cat $tests_path/$test_name/create.sql + return 1 + fi extra_args="" if [ -f $tests_path/$test_name/extra_args ] ; then @@ -257,7 +263,7 @@ build_binary() { test_all() { build_binary - find $tests_path ! -path . -type d -mindepth 1 -maxdepth 1 | cut -d "/" -f 3 | egrep "$test_pattern" | while read test_name ; do + find $tests_path ! -path . -type d -mindepth 1 -maxdepth 1 | cut -d "/" -f 3 | egrep "$test_pattern" | sort | while read test_name ; do test_single "$test_name" if [ $? -ne 0 ] ; then create_statement=$(gh-ost-test-mysql-replica test -t -e "show create table _gh_ost_test_gho \G") diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index 0c37b27..c4dbfd2 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -61,7 +61,7 @@ test_mysql_version() { gh-ost-test-mysql-master -uroot -e "grant all on *.* to 'gh-ost'@'%'" echo "### Running gh-ost tests for $mysql_version" - bash -x ./localtests/test.sh -b bin/gh-ost + ./localtests/test.sh -b bin/gh-ost find sandboxes -name "stop_all" | bash } From 8abd58482651750fc68f9541b76b07b6a16ae50d Mon Sep 17 00:00:00 2001 From: dm-2 <45519614+dm-2@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:49:04 +0000 Subject: [PATCH 20/30] fix broken test by removing invalid insert statement --- localtests/convert-utf8mb4/create.sql | 3 --- 1 file changed, 3 deletions(-) diff --git a/localtests/convert-utf8mb4/create.sql b/localtests/convert-utf8mb4/create.sql index 05f1a13..e35e688 100644 --- a/localtests/convert-utf8mb4/create.sql +++ b/localtests/convert-utf8mb4/create.sql @@ -7,9 +7,6 @@ create table gh_ost_test ( primary key(id) ) auto_increment=1; -insert into gh_ost_test values (null, 'átesting'); - - insert into gh_ost_test values (null, 'Hello world, Καλημέρα κόσμε, コンニチハ', 'átesting0', 'initial'); drop event if exists gh_ost_test; From f053ccd9a6002cabd18105ea2c207f9f62960642 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Wed, 23 Nov 2022 17:32:53 +0800 Subject: [PATCH 21/30] support rocksdb as transactional engine --- doc/command-line-flags.md | 3 +++ go/base/context.go | 17 +++++++++++++++-- go/cmd/gh-ost/main.go | 5 +++++ go/mysql/connection.go | 7 ++++--- go/mysql/connection_test.go | 15 ++++++++++----- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index 56bc642..5a61631 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -246,6 +246,9 @@ Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but `--ssl-key=/path/to/ssl-key.key`: SSL private key file (in PEM format). +### storage-engine +default is `innodb`. When set to `rocksdb`, some necessary changes (e.g. sets isolation level to READ_COMMITTED) is made to support rocksdb as transactional engine. + ### test-on-replica Issue the migration on a replica; do not modify data on master. Useful for validating, testing and benchmarking. See [`testing-on-replica`](testing-on-replica.md) diff --git a/go/base/context.go b/go/base/context.go index 7089874..cc5844e 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -270,8 +270,6 @@ func NewMigrationContext() *MigrationContext { Uuid: uuid.NewV4().String(), defaultNumRetries: 60, ChunkSize: 1000, - InspectorConnectionConfig: mysql.NewConnectionConfig(), - ApplierConnectionConfig: mysql.NewConnectionConfig(), MaxLagMillisecondsThrottleThreshold: 1500, CutOverLockTimeoutSeconds: 3, DMLBatchSize: 10, @@ -290,6 +288,21 @@ func NewMigrationContext() *MigrationContext { } } +func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { + transactionIsolation := "REPEATABLE-READ" + switch storageEngine { + case "rocksdb": + transactionIsolation = "READ-COMMITTED" + case "innodb": + transactionIsolation = "REPEATABLE-READ" + default: + transactionIsolation = "REPEATABLE-READ" + } + this.InspectorConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) + this.ApplierConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) + return nil +} + func getSafeTableName(baseName string, suffix string) string { name := fmt.Sprintf("_%s_%s", baseName, suffix) if len(name) <= mysql.MaxTableNameLength { diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index c00b206..54083c3 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -68,6 +68,7 @@ func main() { flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)") flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)") flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", false, "Attempt to use instant DDL for this migration first") + storageEngine := flag.String("storage-engine", "innodb", "Specify table storage engine (default: 'innodb'). When 'rocksdb': change session transaction isolation level to READ_COMMITTED.") flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)") flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy") @@ -248,6 +249,10 @@ func main() { migrationContext.Log.Warning("--replication-lag-query is deprecated") } + if err := migrationContext.SetConnectionConfig(*storageEngine); err != nil { + migrationContext.Log.Fatale(err) + } + switch *cutOver { case "atomic", "default", "": migrationContext.CutOverType = base.CutOverAtomic diff --git a/go/mysql/connection.go b/go/mysql/connection.go index 6a5c890..dcf33ad 100644 --- a/go/mysql/connection.go +++ b/go/mysql/connection.go @@ -18,7 +18,6 @@ import ( ) const ( - transactionIsolation = "REPEATABLE-READ" TLS_CONFIG_KEY = "ghost" ) @@ -30,11 +29,13 @@ type ConnectionConfig struct { ImpliedKey *InstanceKey tlsConfig *tls.Config Timeout float64 + transactionIsolation string } -func NewConnectionConfig() *ConnectionConfig { +func NewConnectionConfig(transactionIsolation string) *ConnectionConfig { config := &ConnectionConfig{ Key: InstanceKey{}, + transactionIsolation: transactionIsolation, } config.ImpliedKey = &config.Key return config @@ -126,7 +127,7 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string { "charset=utf8mb4,utf8,latin1", "interpolateParams=true", fmt.Sprintf("tls=%s", tlsOption), - fmt.Sprintf("transaction_isolation=%q", transactionIsolation), + fmt.Sprintf("transaction_isolation=%q", this.transactionIsolation), fmt.Sprintf("timeout=%fs", this.Timeout), fmt.Sprintf("readTimeout=%fs", this.Timeout), fmt.Sprintf("writeTimeout=%fs", this.Timeout), diff --git a/go/mysql/connection_test.go b/go/mysql/connection_test.go index 390774c..2f39c4f 100644 --- a/go/mysql/connection_test.go +++ b/go/mysql/connection_test.go @@ -13,12 +13,17 @@ import ( test "github.com/openark/golib/tests" ) +const ( + transactionIsolation = "REPEATABLE-READ" +) + + func init() { log.SetLevel(log.ERROR) } func TestNewConnectionConfig(t *testing.T) { - c := NewConnectionConfig() + c := NewConnectionConfig(transactionIsolation) test.S(t).ExpectEquals(c.Key.Hostname, "") test.S(t).ExpectEquals(c.Key.Port, 0) test.S(t).ExpectEquals(c.ImpliedKey.Hostname, "") @@ -28,7 +33,7 @@ func TestNewConnectionConfig(t *testing.T) { } func TestDuplicateCredentials(t *testing.T) { - c := NewConnectionConfig() + c := NewConnectionConfig(transactionIsolation) c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" @@ -48,7 +53,7 @@ func TestDuplicateCredentials(t *testing.T) { } func TestDuplicate(t *testing.T) { - c := NewConnectionConfig() + c := NewConnectionConfig(transactionIsolation) c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" @@ -63,7 +68,7 @@ func TestDuplicate(t *testing.T) { } func TestGetDBUri(t *testing.T) { - c := NewConnectionConfig() + c := NewConnectionConfig(transactionIsolation) c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" @@ -74,7 +79,7 @@ func TestGetDBUri(t *testing.T) { } func TestGetDBUriWithTLSSetup(t *testing.T) { - c := NewConnectionConfig() + c := NewConnectionConfig(transactionIsolation) c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" From 3ee667a40e3495ce9842e879d6b46e0e76ad437e Mon Sep 17 00:00:00 2001 From: lukelewang Date: Fri, 25 Nov 2022 17:18:32 +0800 Subject: [PATCH 22/30] Modify tests to support rocksdb tests --- localtests/discard-fk/ignore_versions | 1 + localtests/fail-fk-parent/ignore_versions | 1 + localtests/fail-fk/ignore_versions | 1 + .../generated-columns-add/ignore_versions | 1 + .../generated-columns-rename/ignore_versions | 1 + .../generated-columns-unique/ignore_versions | 1 + localtests/generated-columns/ignore_versions | 1 + localtests/geometry/ignore_versions | 1 + localtests/spatial/ignore_versions | 1 + localtests/test.sh | 4 ++ script/cibuild-gh-ost-replica-tests | 40 +++++++++++++++++++ 11 files changed, 53 insertions(+) create mode 100644 localtests/discard-fk/ignore_versions create mode 100644 localtests/fail-fk-parent/ignore_versions create mode 100644 localtests/fail-fk/ignore_versions create mode 100644 localtests/generated-columns-add/ignore_versions create mode 100644 localtests/generated-columns-rename/ignore_versions create mode 100644 localtests/generated-columns-unique/ignore_versions create mode 100644 localtests/generated-columns/ignore_versions create mode 100644 localtests/geometry/ignore_versions create mode 100644 localtests/spatial/ignore_versions diff --git a/localtests/discard-fk/ignore_versions b/localtests/discard-fk/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/discard-fk/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/fail-fk-parent/ignore_versions b/localtests/fail-fk-parent/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/fail-fk-parent/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/fail-fk/ignore_versions b/localtests/fail-fk/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/fail-fk/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/generated-columns-add/ignore_versions b/localtests/generated-columns-add/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/generated-columns-add/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/generated-columns-rename/ignore_versions b/localtests/generated-columns-rename/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/generated-columns-rename/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/generated-columns-unique/ignore_versions b/localtests/generated-columns-unique/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/generated-columns-unique/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/generated-columns/ignore_versions b/localtests/generated-columns/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/generated-columns/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/geometry/ignore_versions b/localtests/geometry/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/geometry/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/spatial/ignore_versions b/localtests/spatial/ignore_versions new file mode 100644 index 0000000..cf02abe --- /dev/null +++ b/localtests/spatial/ignore_versions @@ -0,0 +1 @@ +Percona \ No newline at end of file diff --git a/localtests/test.sh b/localtests/test.sh index 5ca06ad..d73dab2 100755 --- a/localtests/test.sh +++ b/localtests/test.sh @@ -99,9 +99,13 @@ test_single() { if [ -f $tests_path/$test_name/ignore_versions ] ; then ignore_versions=$(cat $tests_path/$test_name/ignore_versions) mysql_version=$(gh-ost-test-mysql-master -s -s -e "select @@version") + mysql_version_comment=$(gh-ost-test-mysql-master -s -s -e "select @@version_comment") if echo "$mysql_version" | egrep -q "^${ignore_versions}" ; then echo -n "Skipping: $test_name" return 0 + elif echo "$mysql_version_comment" | egrep -i -q "^${ignore_versions}" ; then + echo -n "Skipping: $test_name" + return 0 fi fi diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index c4dbfd2..bb60d71 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -60,6 +60,46 @@ test_mysql_version() { gh-ost-test-mysql-master -uroot -e "create user 'gh-ost'@'%' identified by 'gh-ost'" gh-ost-test-mysql-master -uroot -e "grant all on *.* to 'gh-ost'@'%'" + local mysql_server=${mysql_version%-*} + if echo "$mysql_server" | egrep -i "percona" ; then + echo "### Preparing for rocksdb in PerconaServer" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_CFSTATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DBSTATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_HISTORY SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_ACTIVE_COMPACTION_STATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DDL SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_LOCKS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_TRX SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-master -uroot -e "set global default_storage_engine='ROCKSDB'" + gh-ost-test-mysql-master -uroot -e "set global transaction_isolation='READ-COMMITTED'" + + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_CFSTATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DBSTATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_HISTORY SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_ACTIVE_COMPACTION_STATS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DDL SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_LOCKS SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_TRX SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME 'ha_rocksdb.so'" + gh-ost-test-mysql-replica -uroot -e "set global default_storage_engine='ROCKSDB'" + gh-ost-test-mysql-replica -uroot -e "set global transaction_isolation='READ-COMMITTED'" + fi + echo "### Running gh-ost tests for $mysql_version" ./localtests/test.sh -b bin/gh-ost From ccf5b2e01df93ce1d0df9ca3b5bb199dc7c46fb2 Mon Sep 17 00:00:00 2001 From: wangzihuacool Date: Fri, 25 Nov 2022 09:39:57 +0000 Subject: [PATCH 23/30] gofmt --- go/mysql/connection.go | 16 ++++++++-------- go/mysql/connection_test.go | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/go/mysql/connection.go b/go/mysql/connection.go index dcf33ad..15d7cb0 100644 --- a/go/mysql/connection.go +++ b/go/mysql/connection.go @@ -18,23 +18,23 @@ import ( ) const ( - TLS_CONFIG_KEY = "ghost" + TLS_CONFIG_KEY = "ghost" ) // ConnectionConfig is the minimal configuration required to connect to a MySQL server type ConnectionConfig struct { - Key InstanceKey - User string - Password string - ImpliedKey *InstanceKey - tlsConfig *tls.Config - Timeout float64 + Key InstanceKey + User string + Password string + ImpliedKey *InstanceKey + tlsConfig *tls.Config + Timeout float64 transactionIsolation string } func NewConnectionConfig(transactionIsolation string) *ConnectionConfig { config := &ConnectionConfig{ - Key: InstanceKey{}, + Key: InstanceKey{}, transactionIsolation: transactionIsolation, } config.ImpliedKey = &config.Key diff --git a/go/mysql/connection_test.go b/go/mysql/connection_test.go index 2f39c4f..e8cdc27 100644 --- a/go/mysql/connection_test.go +++ b/go/mysql/connection_test.go @@ -17,7 +17,6 @@ const ( transactionIsolation = "REPEATABLE-READ" ) - func init() { log.SetLevel(log.ERROR) } From 9c7857bd469b204399923a102c2ab120495d01c9 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Fri, 25 Nov 2022 18:48:48 +0800 Subject: [PATCH 24/30] SetConnectionConfig --- go/base/context.go | 5 ++++- go/cmd/gh-ost/main.go | 14 +++++++------- go/mysql/connection.go | 16 ++++++++-------- go/mysql/connection_test.go | 1 - 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/go/base/context.go b/go/base/context.go index cc5844e..b68bf7b 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -288,7 +288,7 @@ func NewMigrationContext() *MigrationContext { } } -func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { +func (this *MigrationContext) SetConnectionConfig(storageEngine string, host string, port int, timeout float64) error { transactionIsolation := "REPEATABLE-READ" switch storageEngine { case "rocksdb": @@ -299,6 +299,9 @@ func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { transactionIsolation = "REPEATABLE-READ" } this.InspectorConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) + this.InspectorConnectionConfig.Key.Hostname = host + this.InspectorConnectionConfig.Key.Port = port + this.InspectorConnectionConfig.Timeout = timeout this.ApplierConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) return nil } diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 54083c3..145ef65 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -47,10 +47,10 @@ func acceptSignals(migrationContext *base.MigrationContext) { // main is the application's entry point. It will either spawn a CLI or HTTP interfaces. func main() { migrationContext := base.NewMigrationContext() - flag.StringVar(&migrationContext.InspectorConnectionConfig.Key.Hostname, "host", "127.0.0.1", "MySQL hostname (preferably a replica, not the master)") + host := flag.String("host", "127.0.0.1", "MySQL hostname (preferably a replica, not the master)") + port := flag.Int("port", 3306, "MySQL port (preferably a replica, not the master)") + timeout := flag.Float64("mysql-timeout", 0.0, "Connect, read and write timeout for MySQL") flag.StringVar(&migrationContext.AssumeMasterHostname, "assume-master-host", "", "(optional) explicitly tell gh-ost the identity of the master. Format: some.host.com[:port] This is useful in master-master setups where you wish to pick an explicit master, or in a tungsten-replicator where gh-ost is unable to determine the master") - flag.IntVar(&migrationContext.InspectorConnectionConfig.Key.Port, "port", 3306, "MySQL port (preferably a replica, not the master)") - flag.Float64Var(&migrationContext.InspectorConnectionConfig.Timeout, "mysql-timeout", 0.0, "Connect, read and write timeout for MySQL") flag.StringVar(&migrationContext.CliUser, "user", "", "MySQL user") flag.StringVar(&migrationContext.CliPassword, "password", "", "MySQL password") flag.StringVar(&migrationContext.CliMasterUser, "master-user", "", "MySQL user on master, if different from that on replica. Requires --assume-master-host") @@ -183,6 +183,10 @@ func main() { migrationContext.Log.SetLevel(log.ERROR) } + if err := migrationContext.SetConnectionConfig(*storageEngine, *host, *port, *timeout); err != nil { + migrationContext.Log.Fatale(err) + } + if migrationContext.AlterStatement == "" { log.Fatal("--alter must be provided and statement must not be empty") } @@ -249,10 +253,6 @@ func main() { migrationContext.Log.Warning("--replication-lag-query is deprecated") } - if err := migrationContext.SetConnectionConfig(*storageEngine); err != nil { - migrationContext.Log.Fatale(err) - } - switch *cutOver { case "atomic", "default", "": migrationContext.CutOverType = base.CutOverAtomic diff --git a/go/mysql/connection.go b/go/mysql/connection.go index dcf33ad..15d7cb0 100644 --- a/go/mysql/connection.go +++ b/go/mysql/connection.go @@ -18,23 +18,23 @@ import ( ) const ( - TLS_CONFIG_KEY = "ghost" + TLS_CONFIG_KEY = "ghost" ) // ConnectionConfig is the minimal configuration required to connect to a MySQL server type ConnectionConfig struct { - Key InstanceKey - User string - Password string - ImpliedKey *InstanceKey - tlsConfig *tls.Config - Timeout float64 + Key InstanceKey + User string + Password string + ImpliedKey *InstanceKey + tlsConfig *tls.Config + Timeout float64 transactionIsolation string } func NewConnectionConfig(transactionIsolation string) *ConnectionConfig { config := &ConnectionConfig{ - Key: InstanceKey{}, + Key: InstanceKey{}, transactionIsolation: transactionIsolation, } config.ImpliedKey = &config.Key diff --git a/go/mysql/connection_test.go b/go/mysql/connection_test.go index 2f39c4f..e8cdc27 100644 --- a/go/mysql/connection_test.go +++ b/go/mysql/connection_test.go @@ -17,7 +17,6 @@ const ( transactionIsolation = "REPEATABLE-READ" ) - func init() { log.SetLevel(log.ERROR) } From da3514253f5cd848c1cbcaad63ad794427ed1833 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Sat, 26 Nov 2022 01:38:12 +0800 Subject: [PATCH 25/30] add support for rocksdb --- go/base/context.go | 11 ++-- go/cmd/gh-ost/main.go | 8 +-- go/mysql/connection.go | 20 +++---- go/mysql/connection_test.go | 17 ++++-- localtests/test.sh | 11 ++-- script/cibuild-gh-ost-replica-tests | 86 +++++++++++++++-------------- 6 files changed, 83 insertions(+), 70 deletions(-) diff --git a/go/base/context.go b/go/base/context.go index b68bf7b..be28ac2 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -270,6 +270,8 @@ func NewMigrationContext() *MigrationContext { Uuid: uuid.NewV4().String(), defaultNumRetries: 60, ChunkSize: 1000, + InspectorConnectionConfig: mysql.NewConnectionConfig(), + ApplierConnectionConfig: mysql.NewConnectionConfig(), MaxLagMillisecondsThrottleThreshold: 1500, CutOverLockTimeoutSeconds: 3, DMLBatchSize: 10, @@ -288,7 +290,7 @@ func NewMigrationContext() *MigrationContext { } } -func (this *MigrationContext) SetConnectionConfig(storageEngine string, host string, port int, timeout float64) error { +func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { transactionIsolation := "REPEATABLE-READ" switch storageEngine { case "rocksdb": @@ -298,11 +300,8 @@ func (this *MigrationContext) SetConnectionConfig(storageEngine string, host str default: transactionIsolation = "REPEATABLE-READ" } - this.InspectorConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) - this.InspectorConnectionConfig.Key.Hostname = host - this.InspectorConnectionConfig.Key.Port = port - this.InspectorConnectionConfig.Timeout = timeout - this.ApplierConnectionConfig = mysql.NewConnectionConfig(transactionIsolation) + this.InspectorConnectionConfig.TransactionIsolation = transactionIsolation + this.ApplierConnectionConfig.TransactionIsolation = transactionIsolation return nil } diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 145ef65..98310f3 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -47,10 +47,10 @@ func acceptSignals(migrationContext *base.MigrationContext) { // main is the application's entry point. It will either spawn a CLI or HTTP interfaces. func main() { migrationContext := base.NewMigrationContext() - host := flag.String("host", "127.0.0.1", "MySQL hostname (preferably a replica, not the master)") - port := flag.Int("port", 3306, "MySQL port (preferably a replica, not the master)") - timeout := flag.Float64("mysql-timeout", 0.0, "Connect, read and write timeout for MySQL") + flag.StringVar(&migrationContext.InspectorConnectionConfig.Key.Hostname, "host", "127.0.0.1", "MySQL hostname (preferably a replica, not the master)") flag.StringVar(&migrationContext.AssumeMasterHostname, "assume-master-host", "", "(optional) explicitly tell gh-ost the identity of the master. Format: some.host.com[:port] This is useful in master-master setups where you wish to pick an explicit master, or in a tungsten-replicator where gh-ost is unable to determine the master") + flag.IntVar(&migrationContext.InspectorConnectionConfig.Key.Port, "port", 3306, "MySQL port (preferably a replica, not the master)") + flag.Float64Var(&migrationContext.InspectorConnectionConfig.Timeout, "mysql-timeout", 0.0, "Connect, read and write timeout for MySQL") flag.StringVar(&migrationContext.CliUser, "user", "", "MySQL user") flag.StringVar(&migrationContext.CliPassword, "password", "", "MySQL password") flag.StringVar(&migrationContext.CliMasterUser, "master-user", "", "MySQL user on master, if different from that on replica. Requires --assume-master-host") @@ -183,7 +183,7 @@ func main() { migrationContext.Log.SetLevel(log.ERROR) } - if err := migrationContext.SetConnectionConfig(*storageEngine, *host, *port, *timeout); err != nil { + if err := migrationContext.SetConnectionConfig(*storageEngine); err != nil { migrationContext.Log.Fatale(err) } diff --git a/go/mysql/connection.go b/go/mysql/connection.go index 15d7cb0..6250925 100644 --- a/go/mysql/connection.go +++ b/go/mysql/connection.go @@ -29,13 +29,12 @@ type ConnectionConfig struct { ImpliedKey *InstanceKey tlsConfig *tls.Config Timeout float64 - transactionIsolation string + TransactionIsolation string } -func NewConnectionConfig(transactionIsolation string) *ConnectionConfig { +func NewConnectionConfig() *ConnectionConfig { config := &ConnectionConfig{ - Key: InstanceKey{}, - transactionIsolation: transactionIsolation, + Key: InstanceKey{}, } config.ImpliedKey = &config.Key return config @@ -44,11 +43,12 @@ func NewConnectionConfig(transactionIsolation string) *ConnectionConfig { // DuplicateCredentials creates a new connection config with given key and with same credentials as this config func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig { config := &ConnectionConfig{ - Key: key, - User: this.User, - Password: this.Password, - tlsConfig: this.tlsConfig, - Timeout: this.Timeout, + Key: key, + User: this.User, + Password: this.Password, + tlsConfig: this.tlsConfig, + Timeout: this.Timeout, + TransactionIsolation: this.TransactionIsolation, } config.ImpliedKey = &config.Key return config @@ -127,7 +127,7 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string { "charset=utf8mb4,utf8,latin1", "interpolateParams=true", fmt.Sprintf("tls=%s", tlsOption), - fmt.Sprintf("transaction_isolation=%q", this.transactionIsolation), + fmt.Sprintf("transaction_isolation=%q", this.TransactionIsolation), fmt.Sprintf("timeout=%fs", this.Timeout), fmt.Sprintf("readTimeout=%fs", this.Timeout), fmt.Sprintf("writeTimeout=%fs", this.Timeout), diff --git a/go/mysql/connection_test.go b/go/mysql/connection_test.go index e8cdc27..5667235 100644 --- a/go/mysql/connection_test.go +++ b/go/mysql/connection_test.go @@ -22,17 +22,18 @@ func init() { } func TestNewConnectionConfig(t *testing.T) { - c := NewConnectionConfig(transactionIsolation) + c := NewConnectionConfig() test.S(t).ExpectEquals(c.Key.Hostname, "") test.S(t).ExpectEquals(c.Key.Port, 0) test.S(t).ExpectEquals(c.ImpliedKey.Hostname, "") test.S(t).ExpectEquals(c.ImpliedKey.Port, 0) test.S(t).ExpectEquals(c.User, "") test.S(t).ExpectEquals(c.Password, "") + test.S(t).ExpectEquals(c.TransactionIsolation, "") } func TestDuplicateCredentials(t *testing.T) { - c := NewConnectionConfig(transactionIsolation) + c := NewConnectionConfig() c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" @@ -40,6 +41,7 @@ func TestDuplicateCredentials(t *testing.T) { InsecureSkipVerify: true, ServerName: "feathers", } + c.TransactionIsolation = transactionIsolation dup := c.DuplicateCredentials(InstanceKey{Hostname: "otherhost", Port: 3310}) test.S(t).ExpectEquals(dup.Key.Hostname, "otherhost") @@ -49,13 +51,15 @@ func TestDuplicateCredentials(t *testing.T) { test.S(t).ExpectEquals(dup.User, "gromit") test.S(t).ExpectEquals(dup.Password, "penguin") test.S(t).ExpectEquals(dup.tlsConfig, c.tlsConfig) + test.S(t).ExpectEquals(dup.TransactionIsolation, c.TransactionIsolation) } func TestDuplicate(t *testing.T) { - c := NewConnectionConfig(transactionIsolation) + c := NewConnectionConfig() c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" + c.TransactionIsolation = transactionIsolation dup := c.Duplicate() test.S(t).ExpectEquals(dup.Key.Hostname, "myhost") @@ -64,26 +68,29 @@ func TestDuplicate(t *testing.T) { test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3306) test.S(t).ExpectEquals(dup.User, "gromit") test.S(t).ExpectEquals(dup.Password, "penguin") + test.S(t).ExpectEquals(dup.TransactionIsolation, transactionIsolation) } func TestGetDBUri(t *testing.T) { - c := NewConnectionConfig(transactionIsolation) + c := NewConnectionConfig() c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" c.Timeout = 1.2345 + c.TransactionIsolation = transactionIsolation uri := c.GetDBUri("test") test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=false&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`) } func TestGetDBUriWithTLSSetup(t *testing.T) { - c := NewConnectionConfig(transactionIsolation) + c := NewConnectionConfig() c.Key = InstanceKey{Hostname: "myhost", Port: 3306} c.User = "gromit" c.Password = "penguin" c.Timeout = 1.2345 c.tlsConfig = &tls.Config{} + c.TransactionIsolation = transactionIsolation uri := c.GetDBUri("test") test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=ghost&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`) diff --git a/localtests/test.sh b/localtests/test.sh index d73dab2..14ecd83 100755 --- a/localtests/test.sh +++ b/localtests/test.sh @@ -11,6 +11,7 @@ tests_path=$(dirname $0) test_logfile=/tmp/gh-ost-test.log default_ghost_binary=/tmp/gh-ost-test ghost_binary="" +storage_engine=innodb exec_command_file=/tmp/gh-ost-test.bash ghost_structure_output_file=/tmp/gh-ost-test.ghost.structure.sql orig_content_output_file=/tmp/gh-ost-test.orig.content.csv @@ -24,12 +25,13 @@ replica_port= original_sql_mode= OPTIND=1 -while getopts "b:" OPTION +while getopts "b:s:" OPTION do case $OPTION in b) - ghost_binary="$OPTARG" - ;; + ghost_binary="$OPTARG";; + s) + storage_engine="$OPTARG";; esac done shift $((OPTIND-1)) @@ -158,7 +160,8 @@ test_single() { --assume-master-host=${master_host}:${master_port} --database=test \ --table=gh_ost_test \ - --alter='engine=innodb' \ + --storage-engine=${storage_engine} \ + --alter='engine=${storage_engine}' \ --exact-rowcount \ --assume-rbr \ --initially-drop-old-table \ diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index bb60d71..31b3139 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -36,8 +36,16 @@ test_mysql_version() { mkdir -p sandbox/binary rm -rf sandbox/binary/* - gh-ost-ci-env/bin/linux/dbdeployer unpack gh-ost-ci-env/mysql-tarballs/"$mysql_version".tar.xz --sandbox-binary ${PWD}/sandbox/binary - + local mysql_server=${mysql_version%-*} + if echo "$mysql_server" | egrep -i "percona" ; then + tarball_name=Percona-Server-${mysql_version#*-}-12-Linux.x86_64.glibc2.12-minimal.tar.gz + rm -f gh-ost-ci-env/mysql-tarballs/${tarball_name} + ln -s "$mysql_version".tar.xz gh-ost-ci-env/mysql-tarballs/${tarball_name} + gh-ost-ci-env/bin/linux/dbdeployer unpack gh-ost-ci-env/mysql-tarballs/${tarball_name} --sandbox-binary ${PWD}/sandbox/binary + rm -f gh-ost-ci-env/mysql-tarballs/${tarball_name} + else + gh-ost-ci-env/bin/linux/dbdeployer unpack gh-ost-ci-env/mysql-tarballs/"$mysql_version".tar.xz --sandbox-binary ${PWD}/sandbox/binary + fi mkdir -p sandboxes rm -rf sandboxes/* @@ -60,49 +68,45 @@ test_mysql_version() { gh-ost-test-mysql-master -uroot -e "create user 'gh-ost'@'%' identified by 'gh-ost'" gh-ost-test-mysql-master -uroot -e "grant all on *.* to 'gh-ost'@'%'" - local mysql_server=${mysql_version%-*} if echo "$mysql_server" | egrep -i "percona" ; then echo "### Preparing for rocksdb in PerconaServer" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_CFSTATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DBSTATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_HISTORY SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_ACTIVE_COMPACTION_STATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DDL SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_LOCKS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_TRX SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-master -uroot -e "set global default_storage_engine='ROCKSDB'" - gh-ost-test-mysql-master -uroot -e "set global transaction_isolation='READ-COMMITTED'" + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_CFSTATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DBSTATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DDL SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_LOCKS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_TRX SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-master -uroot -e 'set global default_storage_engine="ROCKSDB"' + gh-ost-test-mysql-master -uroot -e 'set global transaction_isolation="READ-COMMITTED"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_CFSTATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DBSTATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DDL SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_LOCKS SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_TRX SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME "ha_rocksdb.so"' + gh-ost-test-mysql-replica -uroot -e 'set global default_storage_engine="ROCKSDB"' + gh-ost-test-mysql-replica -uroot -e 'set global transaction_isolation="READ-COMMITTED"' - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_CFSTATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DBSTATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_HISTORY SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_ACTIVE_COMPACTION_STATS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DDL SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_LOCKS SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_TRX SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME 'ha_rocksdb.so'" - gh-ost-test-mysql-replica -uroot -e "set global default_storage_engine='ROCKSDB'" - gh-ost-test-mysql-replica -uroot -e "set global transaction_isolation='READ-COMMITTED'" + echo "### Running gh-ost tests for $mysql_version" + ./localtests/test.sh -b bin/gh-ost -s rocksdb + else + echo "### Running gh-ost tests for $mysql_version" + ./localtests/test.sh -b bin/gh-ost -s innodb fi - - echo "### Running gh-ost tests for $mysql_version" - ./localtests/test.sh -b bin/gh-ost - find sandboxes -name "stop_all" | bash } From af1e0d647f661e00b05b7701d36b025d535e6e01 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Sat, 26 Nov 2022 01:43:11 +0800 Subject: [PATCH 26/30] add support for rocksdb --- go/base/context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/base/context.go b/go/base/context.go index be28ac2..764a3e6 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -291,7 +291,7 @@ func NewMigrationContext() *MigrationContext { } func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { - transactionIsolation := "REPEATABLE-READ" + var transactionIsolation string switch storageEngine { case "rocksdb": transactionIsolation = "READ-COMMITTED" From ed71099ce667e833533d95b883567babc5166473 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Sat, 26 Nov 2022 13:08:19 +0800 Subject: [PATCH 27/30] add percona to versions in workflows --- .github/workflows/replica-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/replica-tests.yml b/.github/workflows/replica-tests.yml index 0a82f00..f2a52ec 100644 --- a/.github/workflows/replica-tests.yml +++ b/.github/workflows/replica-tests.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - version: [mysql-5.7.25,mysql-8.0.16] + version: [mysql-5.7.25,mysql-8.0.16,PerconaServer-8.0.21] steps: - uses: actions/checkout@v2 From 6e2be1d44fb5e19e4c6ecf044b220810b0c77927 Mon Sep 17 00:00:00 2001 From: lukelewang Date: Sun, 27 Nov 2022 13:54:01 +0800 Subject: [PATCH 28/30] add description and optimize tests --- doc/command-line-flags.md | 11 ++++++++++- go/base/context.go | 2 -- go/cmd/gh-ost/main.go | 3 +++ script/cibuild-gh-ost-replica-tests | 24 ------------------------ 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index 5a61631..82cb884 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -247,7 +247,16 @@ Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but `--ssl-key=/path/to/ssl-key.key`: SSL private key file (in PEM format). ### storage-engine -default is `innodb`. When set to `rocksdb`, some necessary changes (e.g. sets isolation level to READ_COMMITTED) is made to support rocksdb as transactional engine. +default is `innodb`, and `rocksdb`as an optional. InnoDB and RocksDB are both transactional engines, supporting both shared and exclusive row locks. + +But RocksDB currently lacks a few features support compared to InnoDB: +- Gap Locks +- Foreign Key +- Generated Columns +- Spatial +- Geometry + +When `--storage-engine=rocksdb`, `gh-ost` will make some changes necessary (e.g. sets isolation level to `READ_COMMITTED`) to support RocksDB. ### test-on-replica diff --git a/go/base/context.go b/go/base/context.go index 764a3e6..e3472f5 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -295,8 +295,6 @@ func (this *MigrationContext) SetConnectionConfig(storageEngine string) error { switch storageEngine { case "rocksdb": transactionIsolation = "READ-COMMITTED" - case "innodb": - transactionIsolation = "REPEATABLE-READ" default: transactionIsolation = "REPEATABLE-READ" } diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 98310f3..e214306 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -252,6 +252,9 @@ func main() { if *replicationLagQuery != "" { migrationContext.Log.Warning("--replication-lag-query is deprecated") } + if *storageEngine == "rocksdb" { + migrationContext.Log.Warning("RocksDB storage engine support is experimental") + } switch *cutOver { case "atomic", "default", "": diff --git a/script/cibuild-gh-ost-replica-tests b/script/cibuild-gh-ost-replica-tests index 31b3139..90eb856 100755 --- a/script/cibuild-gh-ost-replica-tests +++ b/script/cibuild-gh-ost-replica-tests @@ -71,33 +71,9 @@ test_mysql_version() { if echo "$mysql_server" | egrep -i "percona" ; then echo "### Preparing for rocksdb in PerconaServer" gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_CFSTATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DBSTATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DDL SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_LOCKS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_TRX SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-master -uroot -e 'INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME "ha_rocksdb.so"' gh-ost-test-mysql-master -uroot -e 'set global default_storage_engine="ROCKSDB"' gh-ost-test-mysql-master -uroot -e 'set global transaction_isolation="READ-COMMITTED"' gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_CFSTATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DBSTATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_PERF_CONTEXT_GLOBAL SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_CF_OPTIONS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_GLOBAL_INFO SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_COMPACTION_STATS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DDL SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_INDEX_FILE_MAP SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_LOCKS SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_TRX SONAME "ha_rocksdb.so"' - gh-ost-test-mysql-replica -uroot -e 'INSTALL PLUGIN ROCKSDB_DEADLOCK SONAME "ha_rocksdb.so"' gh-ost-test-mysql-replica -uroot -e 'set global default_storage_engine="ROCKSDB"' gh-ost-test-mysql-replica -uroot -e 'set global transaction_isolation="READ-COMMITTED"' From 9f3cf74444823a53dc2d1a99c6c561a5a8b8abc1 Mon Sep 17 00:00:00 2001 From: wangzihuacool <47876169+wangzihuacool@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:16:58 +0800 Subject: [PATCH 29/30] Apply suggestions from code review Co-authored-by: dm-2 <45519614+dm-2@users.noreply.github.com> --- doc/command-line-flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index 82cb884..021462f 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -247,7 +247,7 @@ Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but `--ssl-key=/path/to/ssl-key.key`: SSL private key file (in PEM format). ### storage-engine -default is `innodb`, and `rocksdb`as an optional. InnoDB and RocksDB are both transactional engines, supporting both shared and exclusive row locks. +Default is `innodb`, and `rocksdb` support is currently experimental. InnoDB and RocksDB are both transactional engines, supporting both shared and exclusive row locks. But RocksDB currently lacks a few features support compared to InnoDB: - Gap Locks From 20af3af283d88ec3825cc99e140d28411ae7418a Mon Sep 17 00:00:00 2001 From: wangzihuacool <47876169+wangzihuacool@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:30:01 +0800 Subject: [PATCH 30/30] Apply suggestions from code review Co-authored-by: Tim Vaillancourt --- go/cmd/gh-ost/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index e214306..3daf244 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -68,7 +68,7 @@ func main() { flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)") flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)") flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", false, "Attempt to use instant DDL for this migration first") - storageEngine := flag.String("storage-engine", "innodb", "Specify table storage engine (default: 'innodb'). When 'rocksdb': change session transaction isolation level to READ_COMMITTED.") + storageEngine := flag.String("storage-engine", "innodb", "Specify table storage engine (default: 'innodb'). When 'rocksdb': the session transaction isolation level is changed from REPEATABLE_READ to READ_COMMITTED.") flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)") flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy")