Skip to content

Commit 0b86dc1

Browse files
authored
go/vt/mysqlctl: run backup init sql after catchup, disable super readonly (#19713)
1 parent d34d835 commit 0b86dc1

7 files changed

Lines changed: 181 additions & 20 deletions

File tree

go/cmd/vtbackup/cli/vtbackup.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ func init() {
217217
utils.SetFlagBoolVar(Main.Flags(), &allowFirstBackup, "allow-first-backup", allowFirstBackup, "Allow this job to take the first backup of an existing shard.")
218218
utils.SetFlagBoolVar(Main.Flags(), &restartBeforeBackup, "restart-before-backup", restartBeforeBackup, "Perform a mysqld clean/full restart after applying binlogs, but before taking the backup. Only makes sense to work around xtrabackup bugs.")
219219
Main.Flags().BoolVar(&upgradeSafe, "upgrade-safe", upgradeSafe, "Whether to use innodb_fast_shutdown=0 for the backup so it is safe to use for MySQL upgrades.")
220-
Main.Flags().StringSliceVar(&initSQLQueries, "init-backup-sql-queries", nil, "Queries to execute before initializing the backup")
221-
Main.Flags().Var((*topoproto.TabletTypeListFlag)(&initSQLTabletTypes), "init-backup-tablet-types", "Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed before initializing the backup")
220+
Main.Flags().StringSliceVar(&initSQLQueries, "init-backup-sql-queries", nil, "Queries to execute after catch-up replication, before initializing the backup")
221+
Main.Flags().Var((*topoproto.TabletTypeListFlag)(&initSQLTabletTypes), "init-backup-tablet-types", "Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed after catch-up replication, before initializing the backup")
222222
Main.Flags().DurationVar(&initSQLTimeout, "init-backup-sql-timeout", initSQLTimeout, "At what point should we time out the init SQL query (--init-backup-sql-queries) work and either fail the backup job (--init-backup-sql-fail-on-error) or continue on with the backup")
223223
Main.Flags().BoolVar(&initSQLFailOnError, "init-backup-sql-fail-on-error", false, "Whether or not to fail the backup if the init SQL queries (--init-backup-sql-queries) fail, which includes if they fail to complete before the specified timeout (--init-backup-sql-timeout)")
224224

@@ -517,11 +517,6 @@ func takeBackup(ctx, backgroundCtx context.Context, topoServer *topo.Server, bac
517517
}
518518
}
519519

520-
// Perform any requested pre backup initialization queries.
521-
if err := mysqlctl.ExecuteBackupInitSQL(ctx, &backupParams); err != nil {
522-
return vterrors.Wrap(err, "failed to execute backup init SQL queries")
523-
}
524-
525520
// We have restored a backup. Now start replication.
526521
if err := resetReplication(ctx, restorePos, mysqld); err != nil {
527522
return fmt.Errorf("error resetting replication: %v", err)
@@ -559,11 +554,6 @@ func takeBackup(ctx, backgroundCtx context.Context, topoServer *topo.Server, bac
559554

560555
log.Info("takeBackup: primary position is: " + primaryPos.String())
561556

562-
// Remember the time when we fetched the primary position, not when we caught
563-
// up to it, so the timestamp on our backup is honest (assuming we make it
564-
// to the goal position).
565-
backupParams.BackupTime = time.Now()
566-
567557
// Wait for replication to catch up.
568558
phase.Set(phaseNameCatchupReplication, int64(1))
569559
defer phase.Set(phaseNameCatchupReplication, int64(0))
@@ -641,13 +631,23 @@ func takeBackup(ctx, backgroundCtx context.Context, topoServer *topo.Server, bac
641631
phaseStatus.Set([]string{phaseNameCatchupReplication, phaseStatusCatchupReplicationStalled}, 0)
642632
phaseStatus.Set([]string{phaseNameCatchupReplication, phaseStatusCatchupReplicationStopped}, 0)
643633

644-
// Re-enable redo logging.
634+
// Re-enable redo logging before running init SQL, so that init SQL
635+
// queries run with full crash safety.
645636
if disabledRedoLog {
646637
if err := mysqld.EnableRedoLog(ctx); err != nil {
647638
return fmt.Errorf("failed to re-enable redo log: %v", err)
648639
}
649640
}
650641

642+
// Perform any requested backup initialization queries after catch-up replication.
643+
if err := mysqlctl.ExecuteBackupInitSQL(ctx, &backupParams); err != nil {
644+
return vterrors.Wrap(err, "failed to execute backup init SQL queries")
645+
}
646+
647+
// Set BackupTime after catch-up and init SQL, so the timestamp on the
648+
// backup postdates any changes made by init SQL queries.
649+
backupParams.BackupTime = time.Now()
650+
651651
if restartBeforeBackup {
652652
restartAt := time.Now()
653653
log.Info("Proceeding with clean MySQL shutdown and startup to flush all buffers.")

go/cmd/vtctldclient/command/backups.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,8 @@ func init() {
362362
}
363363

364364
func addInitSQLFlags(cmd *cobra.Command) {
365-
cmd.Flags().StringSliceVar(&backupOptions.InitSQLQueries, "init-backup-sql-queries", nil, "Queries to execute before initializing the backup")
366-
cmd.Flags().Var((*topoproto.TabletTypeListFlag)(&backupOptions.InitSQLTabletTypes), "init-backup-tablet-types", "Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed before initializing the backup")
365+
cmd.Flags().StringSliceVar(&backupOptions.InitSQLQueries, "init-backup-sql-queries", nil, "Queries to execute before taking the backup")
366+
cmd.Flags().Var((*topoproto.TabletTypeListFlag)(&backupOptions.InitSQLTabletTypes), "init-backup-tablet-types", "Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed before taking the backup")
367367
cmd.Flags().DurationVar(&backupOptions.InitSQLTimeout, "init-backup-sql-timeout", backupOptions.InitSQLTimeout, "At what point should we time out the init SQL query (--init-backup-sql-queries) work and either fail the backup job (--init-backup-sql-fail-on-error) or continue on with the backup")
368368
cmd.Flags().BoolVar(&backupOptions.InitSQLFailOnError, "init-backup-sql-fail-on-error", false, "Whether or not to fail the backup if the init SQL queries (--init-backup-sql-queries) fail, which includes if they fail to complete before the specified timeout (--init-backup-sql-timeout)")
369369
}

go/flags/endtoend/vtbackup.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ Flags:
153153
-h, --help help for vtbackup
154154
--incremental-from-pos string Position, or name of backup from which to create an incremental backup. Default: empty. If given, then this backup becomes an incremental backup from given position or given backup. If value is 'auto', this backup will be taken from the last successful backup position.
155155
--init-backup-sql-fail-on-error Whether or not to fail the backup if the init SQL queries (--init-backup-sql-queries) fail, which includes if they fail to complete before the specified timeout (--init-backup-sql-timeout)
156-
--init-backup-sql-queries strings Queries to execute before initializing the backup
156+
--init-backup-sql-queries strings Queries to execute after catch-up replication, before initializing the backup
157157
--init-backup-sql-timeout duration At what point should we time out the init SQL query (--init-backup-sql-queries) work and either fail the backup job (--init-backup-sql-fail-on-error) or continue on with the backup
158-
--init-backup-tablet-types strings Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed before initializing the backup
158+
--init-backup-tablet-types strings Tablet types used for the backup where the init SQL queries (--init-backup-sql-queries) will be executed after catch-up replication, before initializing the backup
159159
--init-db-name-override string (init parameter) override the name of the db used by vttablet
160160
--init-db-sql-file string path to .sql file to run after mysql_install_db
161161
--init-keyspace string (init parameter) keyspace to use for this tablet

go/vt/mysqlctl/backup.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"github.com/spf13/pflag"
3131

32+
"vitess.io/vitess/go/mysql/sqlerror"
3233
"vitess.io/vitess/go/protoutil"
3334
"vitess.io/vitess/go/textutil"
3435
"vitess.io/vitess/go/vt/log"
@@ -551,6 +552,25 @@ func ExecuteBackupInitSQL(ctx context.Context, params *BackupParams) error {
551552
params.Logger.Infof("Executing init SQL queries %q, with a timeout of %v and fail backup on error set to %t", queriesCSV, initTimeout, params.InitSQL.FailOnError)
552553
initCtx, cancel := context.WithTimeout(ctx, initTimeout)
553554
defer cancel()
555+
// Disable super_read_only before executing init SQL queries, because MySQL
556+
// may have been started with super-read-only enabled via my.cnf.
557+
resetFunc, err := params.Mysqld.SetSuperReadOnly(initCtx, false)
558+
if err != nil {
559+
if sqlErr, ok := err.(*sqlerror.SQLError); ok && sqlErr.Number() == sqlerror.ERUnknownSystemVariable {
560+
params.Logger.Infof("Server does not support super_read_only, continuing with init SQL queries: %v", err)
561+
} else if params.InitSQL.FailOnError {
562+
return vterrors.Wrap(err, "failed to disable super_read_only for init SQL queries")
563+
} else {
564+
params.Logger.Infof("Failed to disable super_read_only for init SQL queries: %v. Will still attempt init SQL queries as fail-on-error is false", err)
565+
}
566+
}
567+
if resetFunc != nil {
568+
defer func() {
569+
if err := resetFunc(); err != nil {
570+
params.Logger.Errorf("Failed to reset super_read_only after init SQL queries: %v", err)
571+
}
572+
}()
573+
}
554574
if err := params.Mysqld.ExecuteSuperQueryList(initCtx, params.InitSQL.Queries); err != nil {
555575
if params.InitSQL.FailOnError {
556576
return vterrors.Wrapf(err, "failed to execute init SQL queries %q and instructed to fail backup in this case", queriesCSV)

go/vt/mysqlctl/backup_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"context"
2222
"encoding/json"
23+
"errors"
2324
"fmt"
2425
"io"
2526
"os"
@@ -37,6 +38,7 @@ import (
3738
"vitess.io/vitess/go/mysql"
3839
"vitess.io/vitess/go/mysql/fakesqldb"
3940
"vitess.io/vitess/go/mysql/replication"
41+
"vitess.io/vitess/go/mysql/sqlerror"
4042
"vitess.io/vitess/go/protoutil"
4143
"vitess.io/vitess/go/sqltypes"
4244
"vitess.io/vitess/go/test/utils"
@@ -928,8 +930,121 @@ func TestExecuteBackupInitSQL(t *testing.T) {
928930
wantErrString: "no timeout provided",
929931
wantNoLogMsg: true,
930932
},
933+
{
934+
name: "SetSuperReadOnly failure with FailOnError true",
935+
params: &BackupParams{
936+
TabletType: topodatapb.TabletType_PRIMARY,
937+
InitSQL: &tabletmanagerdatapb.BackupRequest_InitSQL{
938+
Queries: []string{"OPTIMIZE TABLE foo"},
939+
TabletTypes: []topodatapb.TabletType{topodatapb.TabletType_PRIMARY},
940+
Timeout: protoutil.DurationToProto(30 * time.Second),
941+
FailOnError: true,
942+
},
943+
Logger: logutil.NewMemoryLogger(),
944+
},
945+
setupMysqld: func(fmd *FakeMysqlDaemon) {
946+
fmd.SetSuperReadOnlyError = errors.New("access denied")
947+
},
948+
wantErr: true,
949+
wantErrString: "failed to disable super_read_only for init SQL queries",
950+
},
951+
{
952+
name: "SetSuperReadOnly ERUnknownSystemVariable with FailOnError true",
953+
params: &BackupParams{
954+
TabletType: topodatapb.TabletType_PRIMARY,
955+
InitSQL: &tabletmanagerdatapb.BackupRequest_InitSQL{
956+
Queries: []string{"OPTIMIZE TABLE foo"},
957+
TabletTypes: []topodatapb.TabletType{topodatapb.TabletType_PRIMARY},
958+
Timeout: protoutil.DurationToProto(30 * time.Second),
959+
FailOnError: true,
960+
},
961+
Logger: logutil.NewMemoryLogger(),
962+
},
963+
setupMysqld: func(fmd *FakeMysqlDaemon) {
964+
fmd.SetSuperReadOnlyError = sqlerror.NewSQLError(sqlerror.ERUnknownSystemVariable, "", "Unknown system variable 'super_read_only'")
965+
fmd.ExpectedExecuteSuperQueryList = []string{"OPTIMIZE TABLE foo"}
966+
},
967+
wantErr: false,
968+
wantLogMsg: "Server does not support super_read_only",
969+
},
970+
{
971+
name: "SetSuperReadOnly failure with FailOnError false",
972+
params: &BackupParams{
973+
TabletType: topodatapb.TabletType_PRIMARY,
974+
InitSQL: &tabletmanagerdatapb.BackupRequest_InitSQL{
975+
Queries: []string{"OPTIMIZE TABLE foo"},
976+
TabletTypes: []topodatapb.TabletType{topodatapb.TabletType_PRIMARY},
977+
Timeout: protoutil.DurationToProto(30 * time.Second),
978+
FailOnError: false,
979+
},
980+
Logger: logutil.NewMemoryLogger(),
981+
},
982+
setupMysqld: func(fmd *FakeMysqlDaemon) {
983+
fmd.SetSuperReadOnlyError = errors.New("access denied")
984+
fmd.ExpectedExecuteSuperQueryList = []string{"OPTIMIZE TABLE foo"}
985+
},
986+
wantErr: false,
987+
wantLogMsg: "Failed to disable super_read_only for init SQL queries",
988+
},
931989
}
932990

991+
// Test that super_read_only is disabled before query execution and reset after.
992+
t.Run("super_read_only disabled and reset after queries", func(t *testing.T) {
993+
sqldb := fakesqldb.New(t)
994+
defer sqldb.Close()
995+
mysqld := NewFakeMysqlDaemon(sqldb)
996+
defer mysqld.Close()
997+
998+
mysqld.SuperReadOnly.Store(true)
999+
mysqld.ExpectedExecuteSuperQueryList = []string{"OPTIMIZE TABLE foo"}
1000+
superReadOnlyDuringQueries := true
1001+
mysqld.ExecuteSuperQueryListCallback = func() {
1002+
superReadOnlyDuringQueries = mysqld.SuperReadOnly.Load()
1003+
}
1004+
1005+
params := &BackupParams{
1006+
TabletType: topodatapb.TabletType_PRIMARY,
1007+
InitSQL: &tabletmanagerdatapb.BackupRequest_InitSQL{
1008+
Queries: []string{"OPTIMIZE TABLE foo"},
1009+
TabletTypes: []topodatapb.TabletType{topodatapb.TabletType_PRIMARY},
1010+
Timeout: protoutil.DurationToProto(30 * time.Second),
1011+
},
1012+
Mysqld: mysqld,
1013+
Logger: logutil.NewMemoryLogger(),
1014+
}
1015+
1016+
err := ExecuteBackupInitSQL(context.Background(), params)
1017+
require.NoError(t, err)
1018+
assert.False(t, superReadOnlyDuringQueries, "super_read_only should be disabled during query execution")
1019+
assert.True(t, mysqld.SuperReadOnly.Load(), "super_read_only should be reset to true after queries complete")
1020+
})
1021+
1022+
// Test that super_read_only is not reset when it was already off.
1023+
t.Run("super_read_only already off no reset needed", func(t *testing.T) {
1024+
sqldb := fakesqldb.New(t)
1025+
defer sqldb.Close()
1026+
mysqld := NewFakeMysqlDaemon(sqldb)
1027+
defer mysqld.Close()
1028+
1029+
mysqld.SuperReadOnly.Store(false)
1030+
mysqld.ExpectedExecuteSuperQueryList = []string{"OPTIMIZE TABLE foo"}
1031+
1032+
params := &BackupParams{
1033+
TabletType: topodatapb.TabletType_PRIMARY,
1034+
InitSQL: &tabletmanagerdatapb.BackupRequest_InitSQL{
1035+
Queries: []string{"OPTIMIZE TABLE foo"},
1036+
TabletTypes: []topodatapb.TabletType{topodatapb.TabletType_PRIMARY},
1037+
Timeout: protoutil.DurationToProto(30 * time.Second),
1038+
},
1039+
Mysqld: mysqld,
1040+
Logger: logutil.NewMemoryLogger(),
1041+
}
1042+
1043+
err := ExecuteBackupInitSQL(context.Background(), params)
1044+
require.NoError(t, err)
1045+
assert.False(t, mysqld.SuperReadOnly.Load(), "super_read_only should remain false")
1046+
})
1047+
9331048
// Test case for context cancellation during query execution.
9341049
t.Run("parent context canceled with query failure", func(t *testing.T) {
9351050
sqldb := fakesqldb.New(t)

go/vt/mysqlctl/fakemysqldaemon.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ type FakeMysqlDaemon struct {
125125
// SuperReadOnly is the current value of the flag.
126126
SuperReadOnly atomic.Bool
127127

128+
// SetSuperReadOnlyError is used by SetSuperReadOnly.
129+
SetSuperReadOnlyError error
130+
131+
// ExecuteSuperQueryListCallback is called at the start of ExecuteSuperQueryList
132+
// before any queries are executed, if set.
133+
ExecuteSuperQueryListCallback func()
134+
128135
// SetReplicationPositionPos is matched against the input of
129136
// SetReplicationPosition. If it doesn't match, SetReplicationPosition
130137
// will return an error.
@@ -456,9 +463,25 @@ func (fmd *FakeMysqlDaemon) SetReadOnly(ctx context.Context, on bool) error {
456463

457464
// SetSuperReadOnly is part of the MysqlDaemon interface.
458465
func (fmd *FakeMysqlDaemon) SetSuperReadOnly(ctx context.Context, on bool) (ResetSuperReadOnlyFunc, error) {
466+
if fmd.SetSuperReadOnlyError != nil {
467+
return nil, fmd.SetSuperReadOnlyError
468+
}
469+
prev := fmd.SuperReadOnly.Load()
470+
prevReadOnly := fmd.ReadOnly
459471
fmd.SuperReadOnly.Store(on)
460-
fmd.ReadOnly = on
461-
return nil, nil
472+
// In real MySQL, enabling super_read_only implies read_only = ON,
473+
// but disabling super_read_only does not change read_only.
474+
if on {
475+
fmd.ReadOnly = true
476+
}
477+
if prev == on {
478+
return nil, nil
479+
}
480+
return func() error {
481+
fmd.SuperReadOnly.Store(prev)
482+
fmd.ReadOnly = prevReadOnly
483+
return nil
484+
}, nil
462485
}
463486

464487
// GetGlobalStatusVars is part of the MysqlDaemon interface.
@@ -593,6 +616,9 @@ func (fmd *FakeMysqlDaemon) ExecuteSuperQuery(ctx context.Context, query string)
593616

594617
// ExecuteSuperQueryList is part of the MysqlDaemon interface
595618
func (fmd *FakeMysqlDaemon) ExecuteSuperQueryList(ctx context.Context, queryList []string) error {
619+
if fmd.ExecuteSuperQueryListCallback != nil {
620+
fmd.ExecuteSuperQueryListCallback()
621+
}
596622
for _, query := range queryList {
597623
// test we still have a query to compare
598624
if fmd.ExpectedExecuteSuperQueryCurrent >= len(fmd.ExpectedExecuteSuperQueryList) {

go/vt/mysqlctl/replication.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func (mysqld *Mysqld) SetSuperReadOnly(ctx context.Context, on bool) (ResetSuper
352352
} else {
353353
query += "'OFF'"
354354
}
355-
if err := mysqld.ExecuteSuperQuery(context.Background(), query); err != nil {
355+
if err := mysqld.ExecuteSuperQuery(ctx, query); err != nil {
356356
return nil, err
357357
}
358358

0 commit comments

Comments
 (0)