diff --git a/go/base/context.go b/go/base/context.go index 52d02dd34..9bd7c677e 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -80,21 +80,22 @@ type MigrationContext struct { OriginalTableName string AlterStatement string - CountTableRows bool - ConcurrentCountTableRows bool - AllowedRunningOnMaster bool - AllowedMasterMaster bool - SwitchToRowBinlogFormat bool - AssumeRBR bool - SkipForeignKeyChecks bool - SkipStrictMode bool - NullableUniqueKeyAllowed bool - ApproveRenamedColumns bool - SkipRenamedColumns bool - IsTungsten bool - DiscardForeignKeys bool - AliyunRDS bool - GoogleCloudPlatform bool + CountTableRows bool + ConcurrentCountTableRows bool + AllowedRunningOnMaster bool + AllowedMasterMaster bool + SwitchToRowBinlogFormat bool + SwitchToFullBinlogRowImage bool + AssumeRBR bool + SkipForeignKeyChecks bool + SkipStrictMode bool + NullableUniqueKeyAllowed bool + ApproveRenamedColumns bool + SkipRenamedColumns bool + IsTungsten bool + DiscardForeignKeys bool + AliyunRDS bool + GoogleCloudPlatform bool config ContextConfig configMutex *sync.Mutex @@ -313,6 +314,11 @@ func (this *MigrationContext) RequiresBinlogFormatChange() bool { return this.OriginalBinlogFormat != "ROW" } +// RequiresBinlogRowImageChange is `true` when the original binlog format isn't `FULL` +func (this *MigrationContext) RequiresBinlogRowImageChange() bool { + return this.OriginalBinlogRowImage != "FULL" +} + // GetApplierHostname is a safe access method to the applier hostname func (this *MigrationContext) GetApplierHostname() string { if this.ApplierConnectionConfig == nil { diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 33fc6f4cb..4feddd012 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -92,6 +92,7 @@ func main() { flag.BoolVar(&migrationContext.ForceNamedPanicCommand, "force-named-panic", false, "When true, the 'panic' interactive command must name the migrated table") flag.BoolVar(&migrationContext.SwitchToRowBinlogFormat, "switch-to-rbr", false, "let this tool automatically switch binary log format to 'ROW' on the replica, if needed. The format will NOT be switched back. I'm too scared to do that, and wish to protect you if you happen to execute another migration while this one is running") + flag.BoolVar(&migrationContext.SwitchToFullBinlogRowImage, "switch-to-full-rbr", false, "let this tool automatically switch binary log row image to 'FULL' on the replica, if needed. The row image will NOT be switched back. I'm too scared to do that, and wish to protect you if you happen to execute another migration while this one is running") flag.BoolVar(&migrationContext.AssumeRBR, "assume-rbr", false, "set to 'true' when you know for certain your server uses 'ROW' binlog_format. gh-ost is unable to tell, event after reading binlog_format, whether the replication process does indeed use 'ROW', and restarts replication to be certain RBR setting is applied. Such operation requires SUPER privileges which you might not have. Setting this flag avoids restarting replication and you can proceed to use gh-ost without SUPER privileges") flag.BoolVar(&migrationContext.CutOverExponentialBackoff, "cut-over-exponential-backoff", false, "Wait exponentially longer intervals between failed cut-over attempts. Wait intervals obey a maximum configurable with 'exponential-backoff-max-interval').") exponentialBackoffMaxInterval := flag.Int64("exponential-backoff-max-interval", 64, "Maximum number of seconds to wait between attempts when performing various operations with exponential backoff.") diff --git a/go/logic/inspect.go b/go/logic/inspect.go index 31184b0c2..b7d761b7b 100644 --- a/go/logic/inspect.go +++ b/go/logic/inspect.go @@ -293,9 +293,11 @@ func (this *Inspector) restartReplication() error { return nil } -// applyBinlogFormat sets ROW binlog format and restarts replication to make +// applyBinlogFormat sets ROW binlog format and FULL binlog row image and restarts replication to make // the replication thread apply it. func (this *Inspector) applyBinlogFormat() error { + changesApplied := false + if this.migrationContext.RequiresBinlogFormatChange() { if !this.migrationContext.SwitchToRowBinlogFormat { return fmt.Errorf("Existing binlog_format is %s. Am not switching it to ROW unless you specify --switch-to-rbr", this.migrationContext.OriginalBinlogFormat) @@ -310,8 +312,28 @@ func (this *Inspector) applyBinlogFormat() error { return err } log.Debugf("'ROW' binlog format applied") + changesApplied = true + } + if this.migrationContext.RequiresBinlogRowImageChange() { + if !this.migrationContext.SwitchToFullBinlogRowImage { + return fmt.Errorf("Existing binlog_row_image is %s. Am not switching it to FULL unless you specify --switch-to-rbr-full", this.migrationContext.OriginalBinlogRowImage) + } + if _, err := sqlutils.ExecNoPrepare(this.db, `set global binlog_row_image='FULL'`); err != nil { + return err + } + if _, err := sqlutils.ExecNoPrepare(this.db, `set session binlog_row_image='FULL'`); err != nil { + return err + } + if err := this.restartReplication(); err != nil { + return err + } + log.Debugf("'FULL' binlog row image applied") + changesApplied = true + } + if changesApplied { return nil } + // We already have RBR, no explicit switch if !this.migrationContext.AssumeRBR { if err := this.restartReplication(); err != nil { @@ -321,6 +343,19 @@ func (this *Inspector) applyBinlogFormat() error { return nil } +// countReplicas returns the number of hosts replicating from `this.db` +func (this *Inspector) countReplicas() (int, error) { + countReplicas := 0 + + query := fmt.Sprintf(`show /* gh-ost */ slave hosts`) + err := sqlutils.QueryRowsMap(this.db, query, func(rowMap sqlutils.RowMap) error { + countReplicas++ + return nil + }) + + return countReplicas, err +} + // validateBinlogs checks that binary log configuration is good to go func (this *Inspector) validateBinlogs() error { query := `select @@global.log_bin, @@global.binlog_format` @@ -335,19 +370,17 @@ func (this *Inspector) validateBinlogs() error { if !this.migrationContext.SwitchToRowBinlogFormat { return fmt.Errorf("You must be using ROW binlog format. I can switch it for you, provided --switch-to-rbr and that %s:%d doesn't have replicas", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port) } - query := fmt.Sprintf(`show /* gh-ost */ slave hosts`) - countReplicas := 0 - err := sqlutils.QueryRowsMap(this.db, query, func(rowMap sqlutils.RowMap) error { - countReplicas++ - return nil - }) + + countReplicas, err := this.countReplicas() if err != nil { return err } + if countReplicas > 0 { - return fmt.Errorf("%s:%d has %s binlog_format, but I'm too scared to change it to ROW because it has replicas. Bailing out", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat) + return fmt.Errorf("%s:%d has '%s' binlog_format, but I'm too scared to change it to ROW because it has replicas. Bailing out", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat) } - log.Infof("%s:%d has %s binlog_format. I will change it to ROW, and will NOT change it back, even in the event of failure.", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat) + + log.Infof("%s:%d has '%s' binlog_format. I will change it to ROW, and will NOT change it back, even in the event of failure.", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogFormat) } query = `select @@global.binlog_row_image` if err := this.db.QueryRow(query).Scan(&this.migrationContext.OriginalBinlogRowImage); err != nil { @@ -356,7 +389,18 @@ func (this *Inspector) validateBinlogs() error { } this.migrationContext.OriginalBinlogRowImage = strings.ToUpper(this.migrationContext.OriginalBinlogRowImage) if this.migrationContext.OriginalBinlogRowImage != "FULL" { - return fmt.Errorf("%s:%d has '%s' binlog_row_image, and only 'FULL' is supported. This operation cannot proceed. You may `set global binlog_row_image='full'` and try again", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogRowImage) + if !this.migrationContext.SwitchToFullBinlogRowImage { + return fmt.Errorf("%s:%d has '%s' binlog_row_image, and only 'FULL' is supported. I can switch it for you, provided --switch-to-full-rbr and that %s:%d doesn't have replicas. Alternatively, you may `set global binlog_row_image='full'` and try again", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogRowImage, this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port) + } + + countReplicas, err := this.countReplicas() + if err != nil { + return err + } + + if countReplicas > 0 { + return fmt.Errorf("%s:%d has '%s' binlog_row_image, but I'm too scared to change it to ROW because it has replicas. Bailing out", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port, this.migrationContext.OriginalBinlogRowImage) + } } log.Infof("binary logs validated on %s:%d", this.connectionConfig.Key.Hostname, this.connectionConfig.Key.Port)