vitess.io/vitess@v0.16.2/go/vt/wrangler/testlib/backup_test.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testlib 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "path" 24 "testing" 25 "time" 26 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 30 "vitess.io/vitess/go/vt/discovery" 31 32 "vitess.io/vitess/go/mysql" 33 "vitess.io/vitess/go/mysql/fakesqldb" 34 "vitess.io/vitess/go/sqltypes" 35 "vitess.io/vitess/go/vt/logutil" 36 "vitess.io/vitess/go/vt/mysqlctl" 37 "vitess.io/vitess/go/vt/mysqlctl/backupstorage" 38 "vitess.io/vitess/go/vt/mysqlctl/filebackupstorage" 39 "vitess.io/vitess/go/vt/topo" 40 "vitess.io/vitess/go/vt/topo/memorytopo" 41 "vitess.io/vitess/go/vt/topo/topoproto" 42 "vitess.io/vitess/go/vt/vttablet/tmclient" 43 "vitess.io/vitess/go/vt/wrangler" 44 45 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 46 ) 47 48 type compressionDetails struct { 49 CompressionEngineName string 50 ExternalCompressorCmd string 51 ExternalCompressorExt string 52 ExternalDecompressorCmd string 53 } 54 55 func TestBackupRestore(t *testing.T) { 56 defer setDefaultCompressionFlag() 57 err := testBackupRestore(t, nil) 58 require.NoError(t, err) 59 } 60 61 func TestBackupRestoreWithPargzip(t *testing.T) { 62 defer setDefaultCompressionFlag() 63 cDetails := &compressionDetails{ 64 CompressionEngineName: "pargzip", 65 } 66 67 err := testBackupRestore(t, cDetails) 68 require.NoError(t, err) 69 } 70 71 func setDefaultCompressionFlag() { 72 mysqlctl.CompressionEngineName = "pgzip" 73 mysqlctl.ExternalCompressorCmd = "" 74 mysqlctl.ExternalCompressorExt = "" 75 mysqlctl.ExternalDecompressorCmd = "" 76 } 77 78 func testBackupRestore(t *testing.T, cDetails *compressionDetails) error { 79 delay := discovery.GetTabletPickerRetryDelay() 80 defer func() { 81 discovery.SetTabletPickerRetryDelay(delay) 82 }() 83 discovery.SetTabletPickerRetryDelay(5 * time.Millisecond) 84 85 // Initialize our environment 86 ctx := context.Background() 87 db := fakesqldb.New(t) 88 defer db.Close() 89 ts := memorytopo.NewServer("cell1", "cell2") 90 wr := wrangler.New(logutil.NewConsoleLogger(), ts, tmclient.NewTabletManagerClient()) 91 vp := NewVtctlPipe(t, ts) 92 defer vp.Close() 93 94 // Set up mock query results. 95 db.AddQuery(mysqlctl.GenerateInitialBinlogEntry(), &sqltypes.Result{}) 96 db.AddQuery("BEGIN", &sqltypes.Result{}) 97 db.AddQuery("COMMIT", &sqltypes.Result{}) 98 db.AddQueryPattern(`SET @@session\.sql_log_bin = .*`, &sqltypes.Result{}) 99 100 // Initialize our temp dirs 101 root := t.TempDir() 102 103 // Initialize BackupStorage 104 fbsRoot := path.Join(root, "fbs") 105 filebackupstorage.FileBackupStorageRoot = fbsRoot 106 backupstorage.BackupStorageImplementation = "file" 107 if cDetails != nil { 108 if cDetails.CompressionEngineName != "" { 109 mysqlctl.CompressionEngineName = cDetails.CompressionEngineName 110 } 111 if cDetails.ExternalCompressorCmd != "" { 112 mysqlctl.ExternalCompressorCmd = cDetails.ExternalCompressorCmd 113 } 114 if cDetails.ExternalCompressorExt != "" { 115 mysqlctl.ExternalCompressorExt = cDetails.ExternalCompressorExt 116 } 117 if cDetails.ExternalDecompressorCmd != "" { 118 mysqlctl.ExternalDecompressorCmd = cDetails.ExternalDecompressorCmd 119 } 120 } 121 122 // Initialize the fake mysql root directories 123 sourceInnodbDataDir := path.Join(root, "source_innodb_data") 124 sourceInnodbLogDir := path.Join(root, "source_innodb_log") 125 sourceDataDir := path.Join(root, "source_data") 126 sourceDataDbDir := path.Join(sourceDataDir, "vt_db") 127 for _, s := range []string{sourceInnodbDataDir, sourceInnodbLogDir, sourceDataDbDir} { 128 require.NoError(t, os.MkdirAll(s, os.ModePerm)) 129 } 130 131 needIt, err := needInnoDBRedoLogSubdir() 132 require.NoError(t, err) 133 if needIt { 134 newPath := path.Join(sourceInnodbLogDir, mysql.DynamicRedoLogSubdir) 135 require.NoError(t, os.Mkdir(newPath, os.ModePerm)) 136 require.NoError(t, os.WriteFile(path.Join(newPath, "#ib_redo1"), []byte("innodb log 1 contents"), os.ModePerm)) 137 } else { 138 require.NoError(t, os.WriteFile(path.Join(sourceInnodbLogDir, "innodb_log_1"), []byte("innodb log 1 contents"), os.ModePerm)) 139 } 140 141 require.NoError(t, os.WriteFile(path.Join(sourceInnodbDataDir, "innodb_data_1"), []byte("innodb data 1 contents"), os.ModePerm)) 142 require.NoError(t, os.WriteFile(path.Join(sourceDataDbDir, "db.opt"), []byte("db opt file"), os.ModePerm)) 143 144 // create a primary tablet, set its primary position 145 primary := NewFakeTablet(t, wr, "cell1", 0, topodatapb.TabletType_PRIMARY, db) 146 primary.FakeMysqlDaemon.ReadOnly = false 147 primary.FakeMysqlDaemon.Replicating = false 148 primary.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 149 GTIDSet: mysql.MariadbGTIDSet{ 150 2: mysql.MariadbGTID{ 151 Domain: 2, 152 Server: 123, 153 Sequence: 457, 154 }, 155 }, 156 } 157 158 // start primary so that replica can fetch primary position from it 159 primary.StartActionLoop(t, wr) 160 defer primary.StopActionLoop(t) 161 162 // create a single tablet, set it up so we can do backups 163 // set its position same as that of primary so that backup doesn't wait for catchup 164 sourceTablet := NewFakeTablet(t, wr, "cell1", 1, topodatapb.TabletType_REPLICA, db) 165 sourceTablet.FakeMysqlDaemon.ReadOnly = true 166 sourceTablet.FakeMysqlDaemon.Replicating = true 167 sourceTablet.FakeMysqlDaemon.SetReplicationSourceInputs = []string{fmt.Sprintf("%s:%d", primary.Tablet.MysqlHostname, primary.Tablet.MysqlPort)} 168 sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 169 GTIDSet: mysql.MariadbGTIDSet{ 170 2: mysql.MariadbGTID{ 171 Domain: 2, 172 Server: 123, 173 Sequence: 457, 174 }, 175 }, 176 } 177 sourceTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 178 // These 4 statements come from tablet startup 179 "STOP SLAVE", 180 "RESET SLAVE ALL", 181 "FAKE SET MASTER", 182 "START SLAVE", 183 // This first set of STOP and START commands come from 184 // the builtinBackupEngine implementation which stops the replication 185 // while taking the backup 186 "STOP SLAVE", 187 "START SLAVE", 188 // These commands come from SetReplicationSource RPC called 189 // to set the correct primary and semi-sync after Backup has concluded 190 "STOP SLAVE", 191 "RESET SLAVE ALL", 192 "FAKE SET MASTER", 193 "START SLAVE", 194 } 195 sourceTablet.StartActionLoop(t, wr) 196 defer sourceTablet.StopActionLoop(t) 197 198 sourceTablet.TM.Cnf = &mysqlctl.Mycnf{ 199 DataDir: sourceDataDir, 200 InnodbDataHomeDir: sourceInnodbDataDir, 201 InnodbLogGroupHomeDir: sourceInnodbLogDir, 202 } 203 204 // run the backup 205 require.NoError(t, vp.Run([]string{"Backup", topoproto.TabletAliasString(sourceTablet.Tablet.Alias)})) 206 207 // verify the full status 208 require.NoError(t, sourceTablet.FakeMysqlDaemon.CheckSuperQueryList()) 209 assert.True(t, sourceTablet.FakeMysqlDaemon.Replicating) 210 assert.True(t, sourceTablet.FakeMysqlDaemon.Running) 211 212 backupTime := time.Now() 213 214 // create a destination tablet, set it up so we can do restores 215 destTablet := NewFakeTablet(t, wr, "cell1", 2, topodatapb.TabletType_REPLICA, db) 216 destTablet.FakeMysqlDaemon.ReadOnly = true 217 destTablet.FakeMysqlDaemon.Replicating = true 218 destTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 219 GTIDSet: mysql.MariadbGTIDSet{ 220 2: mysql.MariadbGTID{ 221 Domain: 2, 222 Server: 123, 223 Sequence: 457, 224 }, 225 }, 226 } 227 destTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 228 // These 4 statements come from tablet startup 229 "STOP SLAVE", 230 "RESET SLAVE ALL", 231 "FAKE SET MASTER", 232 "START SLAVE", 233 "STOP SLAVE", 234 "RESET SLAVE ALL", 235 "FAKE SET SLAVE POSITION", 236 "STOP SLAVE", 237 "RESET SLAVE ALL", 238 "FAKE SET MASTER", 239 "START SLAVE", 240 } 241 destTablet.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 242 "SHOW DATABASES": {}, 243 } 244 destTablet.FakeMysqlDaemon.SetReplicationPositionPos = sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition 245 destTablet.FakeMysqlDaemon.SetReplicationSourceInputs = append(destTablet.FakeMysqlDaemon.SetReplicationSourceInputs, topoproto.MysqlAddr(primary.Tablet)) 246 247 destTablet.StartActionLoop(t, wr) 248 defer destTablet.StopActionLoop(t) 249 250 destTablet.TM.Cnf = &mysqlctl.Mycnf{ 251 DataDir: sourceDataDir, 252 InnodbDataHomeDir: sourceInnodbDataDir, 253 InnodbLogGroupHomeDir: sourceInnodbLogDir, 254 BinLogPath: path.Join(root, "bin-logs/filename_prefix"), 255 RelayLogPath: path.Join(root, "relay-logs/filename_prefix"), 256 RelayLogIndexPath: path.Join(root, "relay-log.index"), 257 RelayLogInfoPath: path.Join(root, "relay-log.info"), 258 } 259 260 err = destTablet.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, time.Time{} /* backupTime */) 261 if err != nil { 262 return err 263 } 264 // verify the full status 265 require.NoError(t, destTablet.FakeMysqlDaemon.CheckSuperQueryList(), "destTablet.FakeMysqlDaemon.CheckSuperQueryList failed") 266 assert.True(t, destTablet.FakeMysqlDaemon.Replicating) 267 assert.True(t, destTablet.FakeMysqlDaemon.Running) 268 269 // Initialize mycnf, required for restore 270 primaryInnodbDataDir := path.Join(root, "primary_innodb_data") 271 primaryInnodbLogDir := path.Join(root, "primary_innodb_log") 272 primaryDataDir := path.Join(root, "primary_data") 273 primary.TM.Cnf = &mysqlctl.Mycnf{ 274 DataDir: primaryDataDir, 275 InnodbDataHomeDir: primaryInnodbDataDir, 276 InnodbLogGroupHomeDir: primaryInnodbLogDir, 277 BinLogPath: path.Join(root, "bin-logs/filename_prefix"), 278 RelayLogPath: path.Join(root, "relay-logs/filename_prefix"), 279 RelayLogIndexPath: path.Join(root, "relay-log.index"), 280 RelayLogInfoPath: path.Join(root, "relay-log.info"), 281 } 282 283 primary.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 284 "SHOW DATABASES": {}, 285 } 286 primary.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 287 "STOP SLAVE", 288 "RESET SLAVE ALL", 289 "FAKE SET SLAVE POSITION", 290 "RESET SLAVE ALL", 291 "FAKE SET MASTER", 292 "START SLAVE", 293 } 294 295 primary.FakeMysqlDaemon.SetReplicationPositionPos = primary.FakeMysqlDaemon.CurrentPrimaryPosition 296 297 // restore primary from latest backup 298 require.NoError(t, primary.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, time.Time{} /* restoreFromBackupTs */), 299 "RestoreData failed") 300 // tablet was created as PRIMARY, so it's baseTabletType is PRIMARY 301 assert.Equal(t, topodatapb.TabletType_PRIMARY, primary.Tablet.Type) 302 assert.False(t, primary.FakeMysqlDaemon.Replicating) 303 assert.True(t, primary.FakeMysqlDaemon.Running) 304 305 // restore primary when database already exists 306 // checkNoDb should return false 307 // so fake the necessary queries 308 primary.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 309 "SHOW DATABASES": {Rows: [][]sqltypes.Value{{sqltypes.NewVarBinary("vt_test_keyspace")}}}, 310 "SHOW TABLES FROM `vt_test_keyspace`": {Rows: [][]sqltypes.Value{{sqltypes.NewVarBinary("a")}}}, 311 } 312 313 // Test restore with the backup timestamp 314 require.NoError(t, primary.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, backupTime), 315 "RestoreData with backup timestamp failed") 316 assert.Equal(t, topodatapb.TabletType_PRIMARY, primary.Tablet.Type) 317 assert.False(t, primary.FakeMysqlDaemon.Replicating) 318 assert.True(t, primary.FakeMysqlDaemon.Running) 319 return nil 320 } 321 322 // TestBackupRestoreLagged tests the changes made in https://github.com/vitessio/vitess/pull/5000 323 // While doing a backup or a restore, we wait for a change of the replica's position before completing the action 324 // This is because otherwise ReplicationLagSeconds is not accurate and the tablet may go into SERVING when it should not 325 func TestBackupRestoreLagged(t *testing.T) { 326 delay := discovery.GetTabletPickerRetryDelay() 327 defer func() { 328 discovery.SetTabletPickerRetryDelay(delay) 329 }() 330 discovery.SetTabletPickerRetryDelay(5 * time.Millisecond) 331 332 // Initialize our environment 333 ctx := context.Background() 334 db := fakesqldb.New(t) 335 defer db.Close() 336 ts := memorytopo.NewServer("cell1", "cell2") 337 wr := wrangler.New(logutil.NewConsoleLogger(), ts, tmclient.NewTabletManagerClient()) 338 vp := NewVtctlPipe(t, ts) 339 defer vp.Close() 340 341 // Set up mock query results. 342 db.AddQuery(mysqlctl.GenerateInitialBinlogEntry(), &sqltypes.Result{}) 343 db.AddQuery("BEGIN", &sqltypes.Result{}) 344 db.AddQuery("COMMIT", &sqltypes.Result{}) 345 db.AddQueryPattern(`SET @@session\.sql_log_bin = .*`, &sqltypes.Result{}) 346 347 // Initialize our temp dirs 348 root := t.TempDir() 349 350 // Initialize BackupStorage 351 fbsRoot := path.Join(root, "fbs") 352 filebackupstorage.FileBackupStorageRoot = fbsRoot 353 backupstorage.BackupStorageImplementation = "file" 354 355 // Initialize the fake mysql root directories 356 sourceInnodbDataDir := path.Join(root, "source_innodb_data") 357 sourceInnodbLogDir := path.Join(root, "source_innodb_log") 358 sourceDataDir := path.Join(root, "source_data") 359 sourceDataDbDir := path.Join(sourceDataDir, "vt_db") 360 for _, s := range []string{sourceInnodbDataDir, sourceInnodbLogDir, sourceDataDbDir} { 361 require.NoError(t, os.MkdirAll(s, os.ModePerm)) 362 } 363 require.NoError(t, os.WriteFile(path.Join(sourceInnodbDataDir, "innodb_data_1"), []byte("innodb data 1 contents"), os.ModePerm)) 364 365 needIt, err := needInnoDBRedoLogSubdir() 366 require.NoError(t, err) 367 if needIt { 368 newPath := path.Join(sourceInnodbLogDir, mysql.DynamicRedoLogSubdir) 369 require.NoError(t, os.Mkdir(newPath, os.ModePerm)) 370 require.NoError(t, os.WriteFile(path.Join(newPath, "#ib_redo1"), []byte("innodb log 1 contents"), os.ModePerm)) 371 } else { 372 require.NoError(t, os.WriteFile(path.Join(sourceInnodbLogDir, "innodb_log_1"), []byte("innodb log 1 contents"), os.ModePerm)) 373 } 374 375 require.NoError(t, os.WriteFile(path.Join(sourceDataDbDir, "db.opt"), []byte("db opt file"), os.ModePerm)) 376 377 // create a primary tablet, set its position 378 primary := NewFakeTablet(t, wr, "cell1", 0, topodatapb.TabletType_PRIMARY, db) 379 primary.FakeMysqlDaemon.ReadOnly = false 380 primary.FakeMysqlDaemon.Replicating = false 381 primary.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 382 GTIDSet: mysql.MariadbGTIDSet{ 383 2: mysql.MariadbGTID{ 384 Domain: 2, 385 Server: 123, 386 Sequence: 457, 387 }, 388 }, 389 } 390 391 // start primary so that replica can fetch primary position from it 392 primary.StartActionLoop(t, wr) 393 defer primary.StopActionLoop(t) 394 395 // create a single tablet, set it up so we can do backups 396 // set its position same as that of primary so that backup doesn't wait for catchup 397 sourceTablet := NewFakeTablet(t, wr, "cell1", 1, topodatapb.TabletType_REPLICA, db) 398 sourceTablet.FakeMysqlDaemon.ReadOnly = true 399 sourceTablet.FakeMysqlDaemon.Replicating = true 400 sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 401 GTIDSet: mysql.MariadbGTIDSet{ 402 2: mysql.MariadbGTID{ 403 Domain: 2, 404 Server: 123, 405 Sequence: 456, 406 }, 407 }, 408 } 409 sourceTablet.FakeMysqlDaemon.SetReplicationSourceInputs = []string{fmt.Sprintf("%s:%d", primary.Tablet.MysqlHostname, primary.Tablet.MysqlPort)} 410 sourceTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 411 // These 4 statements come from tablet startup 412 "STOP SLAVE", 413 "RESET SLAVE ALL", 414 "FAKE SET MASTER", 415 "START SLAVE", 416 // This first set of STOP and START commands come from 417 // the builtinBackupEngine implementation which stops the replication 418 // while taking the backup 419 "STOP SLAVE", 420 "START SLAVE", 421 // These commands come from SetReplicationSource RPC called 422 // to set the correct primary and semi-sync after Backup has concluded 423 "STOP SLAVE", 424 "RESET SLAVE ALL", 425 "FAKE SET MASTER", 426 "START SLAVE", 427 } 428 sourceTablet.StartActionLoop(t, wr) 429 defer sourceTablet.StopActionLoop(t) 430 431 sourceTablet.TM.Cnf = &mysqlctl.Mycnf{ 432 DataDir: sourceDataDir, 433 InnodbDataHomeDir: sourceInnodbDataDir, 434 InnodbLogGroupHomeDir: sourceInnodbLogDir, 435 } 436 437 errCh := make(chan error, 1) 438 go func(ctx context.Context, tablet *FakeTablet) { 439 errCh <- vp.Run([]string{"Backup", topoproto.TabletAliasString(tablet.Tablet.Alias)}) 440 }(ctx, sourceTablet) 441 442 timer := time.NewTicker(1 * time.Second) 443 <-timer.C 444 sourceTablet.FakeMysqlDaemon.CurrentPrimaryPositionLocked(mysql.Position{ 445 GTIDSet: mysql.MariadbGTIDSet{ 446 2: mysql.MariadbGTID{ 447 Domain: 2, 448 Server: 123, 449 Sequence: 457, 450 }, 451 }, 452 }) 453 454 timer2 := time.NewTicker(5 * time.Second) 455 select { 456 case err := <-errCh: 457 require.Nil(t, err) 458 // verify the full status 459 // verify the full status 460 require.NoError(t, sourceTablet.FakeMysqlDaemon.CheckSuperQueryList()) 461 assert.True(t, sourceTablet.FakeMysqlDaemon.Replicating) 462 assert.True(t, sourceTablet.FakeMysqlDaemon.Running) 463 assert.Equal(t, primary.FakeMysqlDaemon.CurrentPrimaryPosition, sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition) 464 case <-timer2.C: 465 require.FailNow(t, "Backup timed out") 466 } 467 468 // create a destination tablet, set it up so we can do restores 469 destTablet := NewFakeTablet(t, wr, "cell1", 2, topodatapb.TabletType_REPLICA, db) 470 destTablet.FakeMysqlDaemon.ReadOnly = true 471 destTablet.FakeMysqlDaemon.Replicating = true 472 destTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 473 GTIDSet: mysql.MariadbGTIDSet{ 474 2: mysql.MariadbGTID{ 475 Domain: 2, 476 Server: 123, 477 Sequence: 456, 478 }, 479 }, 480 } 481 destTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 482 // These 4 statements come from tablet startup 483 "STOP SLAVE", 484 "RESET SLAVE ALL", 485 "FAKE SET MASTER", 486 "START SLAVE", 487 "STOP SLAVE", 488 "RESET SLAVE ALL", 489 "FAKE SET SLAVE POSITION", 490 "STOP SLAVE", 491 "RESET SLAVE ALL", 492 "FAKE SET MASTER", 493 "START SLAVE", 494 } 495 destTablet.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 496 "SHOW DATABASES": {}, 497 } 498 destTablet.FakeMysqlDaemon.SetReplicationPositionPos = destTablet.FakeMysqlDaemon.CurrentPrimaryPosition 499 destTablet.FakeMysqlDaemon.SetReplicationSourceInputs = append(destTablet.FakeMysqlDaemon.SetReplicationSourceInputs, topoproto.MysqlAddr(primary.Tablet)) 500 501 destTablet.StartActionLoop(t, wr) 502 defer destTablet.StopActionLoop(t) 503 504 destTablet.TM.Cnf = &mysqlctl.Mycnf{ 505 DataDir: sourceDataDir, 506 InnodbDataHomeDir: sourceInnodbDataDir, 507 InnodbLogGroupHomeDir: sourceInnodbLogDir, 508 BinLogPath: path.Join(root, "bin-logs/filename_prefix"), 509 RelayLogPath: path.Join(root, "relay-logs/filename_prefix"), 510 RelayLogIndexPath: path.Join(root, "relay-log.index"), 511 RelayLogInfoPath: path.Join(root, "relay-log.info"), 512 } 513 514 errCh = make(chan error, 1) 515 go func(ctx context.Context, tablet *FakeTablet) { 516 errCh <- tablet.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, time.Time{} /* restoreFromBackupTs */) 517 }(ctx, destTablet) 518 519 timer = time.NewTicker(1 * time.Second) 520 <-timer.C 521 destTablet.FakeMysqlDaemon.CurrentPrimaryPositionLocked(mysql.Position{ 522 GTIDSet: mysql.MariadbGTIDSet{ 523 2: mysql.MariadbGTID{ 524 Domain: 2, 525 Server: 123, 526 Sequence: 457, 527 }, 528 }, 529 }) 530 531 timer2 = time.NewTicker(5 * time.Second) 532 select { 533 case err := <-errCh: 534 require.Nil(t, err) 535 // verify the full status 536 require.NoError(t, destTablet.FakeMysqlDaemon.CheckSuperQueryList(), "destTablet.FakeMysqlDaemon.CheckSuperQueryList failed") 537 assert.True(t, destTablet.FakeMysqlDaemon.Replicating) 538 assert.True(t, destTablet.FakeMysqlDaemon.Running) 539 assert.Equal(t, primary.FakeMysqlDaemon.CurrentPrimaryPosition, destTablet.FakeMysqlDaemon.CurrentPrimaryPosition) 540 case <-timer2.C: 541 require.FailNow(t, "Restore timed out") 542 } 543 } 544 545 func TestRestoreUnreachablePrimary(t *testing.T) { 546 delay := discovery.GetTabletPickerRetryDelay() 547 defer func() { 548 discovery.SetTabletPickerRetryDelay(delay) 549 }() 550 discovery.SetTabletPickerRetryDelay(5 * time.Millisecond) 551 552 // Initialize our environment 553 ctx := context.Background() 554 db := fakesqldb.New(t) 555 defer db.Close() 556 ts := memorytopo.NewServer("cell1") 557 wr := wrangler.New(logutil.NewConsoleLogger(), ts, tmclient.NewTabletManagerClient()) 558 vp := NewVtctlPipe(t, ts) 559 defer vp.Close() 560 561 // Set up mock query results. 562 db.AddQuery(mysqlctl.GenerateInitialBinlogEntry(), &sqltypes.Result{}) 563 db.AddQuery("BEGIN", &sqltypes.Result{}) 564 db.AddQuery("COMMIT", &sqltypes.Result{}) 565 db.AddQueryPattern(`SET @@session\.sql_log_bin = .*`, &sqltypes.Result{}) 566 567 // Initialize our temp dirs 568 root := t.TempDir() 569 570 // Initialize BackupStorage 571 fbsRoot := path.Join(root, "fbs") 572 filebackupstorage.FileBackupStorageRoot = fbsRoot 573 backupstorage.BackupStorageImplementation = "file" 574 575 // Initialize the fake mysql root directories 576 sourceInnodbDataDir := path.Join(root, "source_innodb_data") 577 sourceInnodbLogDir := path.Join(root, "source_innodb_log") 578 sourceDataDir := path.Join(root, "source_data") 579 sourceDataDbDir := path.Join(sourceDataDir, "vt_db") 580 for _, s := range []string{sourceInnodbDataDir, sourceInnodbLogDir, sourceDataDbDir} { 581 require.NoError(t, os.MkdirAll(s, os.ModePerm)) 582 } 583 require.NoError(t, os.WriteFile(path.Join(sourceInnodbDataDir, "innodb_data_1"), []byte("innodb data 1 contents"), os.ModePerm)) 584 585 needIt, err := needInnoDBRedoLogSubdir() 586 require.NoError(t, err) 587 if needIt { 588 newPath := path.Join(sourceInnodbLogDir, mysql.DynamicRedoLogSubdir) 589 require.NoError(t, os.Mkdir(newPath, os.ModePerm)) 590 require.NoError(t, os.WriteFile(path.Join(newPath, "#ib_redo1"), []byte("innodb log 1 contents"), os.ModePerm)) 591 } else { 592 require.NoError(t, os.WriteFile(path.Join(sourceInnodbLogDir, "innodb_log_1"), []byte("innodb log 1 contents"), os.ModePerm)) 593 } 594 595 require.NoError(t, os.WriteFile(path.Join(sourceDataDbDir, "db.opt"), []byte("db opt file"), os.ModePerm)) 596 597 // create a primary tablet, set its primary position 598 primary := NewFakeTablet(t, wr, "cell1", 0, topodatapb.TabletType_PRIMARY, db) 599 primary.FakeMysqlDaemon.ReadOnly = false 600 primary.FakeMysqlDaemon.Replicating = false 601 primary.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 602 GTIDSet: mysql.MariadbGTIDSet{ 603 2: mysql.MariadbGTID{ 604 Domain: 2, 605 Server: 123, 606 Sequence: 457, 607 }, 608 }, 609 } 610 611 // start primary so that replica can fetch primary position from it 612 primary.StartActionLoop(t, wr) 613 614 // create a single tablet, set it up so we can do backups 615 // set its position same as that of primary so that backup doesn't wait for catchup 616 sourceTablet := NewFakeTablet(t, wr, "cell1", 1, topodatapb.TabletType_REPLICA, db) 617 sourceTablet.FakeMysqlDaemon.ReadOnly = true 618 sourceTablet.FakeMysqlDaemon.Replicating = true 619 sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 620 GTIDSet: mysql.MariadbGTIDSet{ 621 2: mysql.MariadbGTID{ 622 Domain: 2, 623 Server: 123, 624 Sequence: 457, 625 }, 626 }, 627 } 628 sourceTablet.FakeMysqlDaemon.SetReplicationSourceInputs = []string{fmt.Sprintf("%s:%d", primary.Tablet.MysqlHostname, primary.Tablet.MysqlPort)} 629 sourceTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 630 // These 4 statements come from tablet startup 631 "STOP SLAVE", 632 "RESET SLAVE ALL", 633 "FAKE SET MASTER", 634 "START SLAVE", 635 // This first set of STOP and START commands come from 636 // the builtinBackupEngine implementation which stops the replication 637 // while taking the backup 638 "STOP SLAVE", 639 "START SLAVE", 640 // These commands come from SetReplicationSource RPC called 641 // to set the correct primary and semi-sync after Backup has concluded 642 "STOP SLAVE", 643 "RESET SLAVE ALL", 644 "FAKE SET MASTER", 645 "START SLAVE", 646 } 647 sourceTablet.StartActionLoop(t, wr) 648 defer sourceTablet.StopActionLoop(t) 649 650 sourceTablet.TM.Cnf = &mysqlctl.Mycnf{ 651 DataDir: sourceDataDir, 652 InnodbDataHomeDir: sourceInnodbDataDir, 653 InnodbLogGroupHomeDir: sourceInnodbLogDir, 654 } 655 656 // run the backup 657 require.NoError(t, vp.Run([]string{"Backup", topoproto.TabletAliasString(sourceTablet.Tablet.Alias)})) 658 659 // create a destination tablet, set it up so we can do restores 660 destTablet := NewFakeTablet(t, wr, "cell1", 2, topodatapb.TabletType_REPLICA, db) 661 destTablet.FakeMysqlDaemon.ReadOnly = true 662 destTablet.FakeMysqlDaemon.Replicating = true 663 destTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 664 GTIDSet: mysql.MariadbGTIDSet{ 665 2: mysql.MariadbGTID{ 666 Domain: 2, 667 Server: 123, 668 Sequence: 457, 669 }, 670 }, 671 } 672 destTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 673 // These 4 statements come from tablet startup 674 "STOP SLAVE", 675 "RESET SLAVE ALL", 676 "FAKE SET MASTER", 677 "START SLAVE", 678 "STOP SLAVE", 679 "RESET SLAVE ALL", 680 "FAKE SET SLAVE POSITION", 681 "STOP SLAVE", 682 "RESET SLAVE ALL", 683 "FAKE SET MASTER", 684 "START SLAVE", 685 } 686 destTablet.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 687 "SHOW DATABASES": {}, 688 } 689 destTablet.FakeMysqlDaemon.SetReplicationPositionPos = sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition 690 destTablet.FakeMysqlDaemon.SetReplicationSourceInputs = append(destTablet.FakeMysqlDaemon.SetReplicationSourceInputs, topoproto.MysqlAddr(primary.Tablet)) 691 692 destTablet.StartActionLoop(t, wr) 693 defer destTablet.StopActionLoop(t) 694 695 destTablet.TM.Cnf = &mysqlctl.Mycnf{ 696 DataDir: sourceDataDir, 697 InnodbDataHomeDir: sourceInnodbDataDir, 698 InnodbLogGroupHomeDir: sourceInnodbLogDir, 699 BinLogPath: path.Join(root, "bin-logs/filename_prefix"), 700 RelayLogPath: path.Join(root, "relay-logs/filename_prefix"), 701 RelayLogIndexPath: path.Join(root, "relay-log.index"), 702 RelayLogInfoPath: path.Join(root, "relay-log.info"), 703 } 704 705 // stop primary so that it is unreachable 706 primary.StopActionLoop(t) 707 708 // set a short timeout so that we don't have to wait 30 seconds 709 topo.RemoteOperationTimeout = 2 * time.Second 710 // Restore should still succeed 711 require.NoError(t, destTablet.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, time.Time{} /* restoreFromBackupTs */)) 712 // verify the full status 713 require.NoError(t, destTablet.FakeMysqlDaemon.CheckSuperQueryList(), "destTablet.FakeMysqlDaemon.CheckSuperQueryList failed") 714 assert.True(t, destTablet.FakeMysqlDaemon.Replicating) 715 assert.True(t, destTablet.FakeMysqlDaemon.Running) 716 } 717 718 func TestDisableActiveReparents(t *testing.T) { 719 mysqlctl.DisableActiveReparents = true 720 delay := discovery.GetTabletPickerRetryDelay() 721 defer func() { 722 // When you mess with globals you must remember to reset them 723 mysqlctl.DisableActiveReparents = false 724 discovery.SetTabletPickerRetryDelay(delay) 725 }() 726 discovery.SetTabletPickerRetryDelay(5 * time.Millisecond) 727 728 // Initialize our environment 729 ctx := context.Background() 730 db := fakesqldb.New(t) 731 defer db.Close() 732 ts := memorytopo.NewServer("cell1", "cell2") 733 wr := wrangler.New(logutil.NewConsoleLogger(), ts, tmclient.NewTabletManagerClient()) 734 vp := NewVtctlPipe(t, ts) 735 defer vp.Close() 736 737 // Set up mock query results. 738 db.AddQuery(mysqlctl.GenerateInitialBinlogEntry(), &sqltypes.Result{}) 739 db.AddQuery("BEGIN", &sqltypes.Result{}) 740 db.AddQuery("COMMIT", &sqltypes.Result{}) 741 db.AddQueryPattern(`SET @@session\.sql_log_bin = .*`, &sqltypes.Result{}) 742 743 // Initialize our temp dirs 744 root := t.TempDir() 745 746 // Initialize BackupStorage 747 fbsRoot := path.Join(root, "fbs") 748 filebackupstorage.FileBackupStorageRoot = fbsRoot 749 backupstorage.BackupStorageImplementation = "file" 750 751 // Initialize the fake mysql root directories 752 sourceInnodbDataDir := path.Join(root, "source_innodb_data") 753 sourceInnodbLogDir := path.Join(root, "source_innodb_log") 754 sourceDataDir := path.Join(root, "source_data") 755 sourceDataDbDir := path.Join(sourceDataDir, "vt_db") 756 for _, s := range []string{sourceInnodbDataDir, sourceInnodbLogDir, sourceDataDbDir} { 757 require.NoError(t, os.MkdirAll(s, os.ModePerm)) 758 } 759 require.NoError(t, os.WriteFile(path.Join(sourceInnodbDataDir, "innodb_data_1"), []byte("innodb data 1 contents"), os.ModePerm)) 760 761 needIt, err := needInnoDBRedoLogSubdir() 762 require.NoError(t, err) 763 if needIt { 764 newPath := path.Join(sourceInnodbLogDir, mysql.DynamicRedoLogSubdir) 765 require.NoError(t, os.Mkdir(newPath, os.ModePerm)) 766 require.NoError(t, os.WriteFile(path.Join(newPath, "#ib_redo1"), []byte("innodb log 1 contents"), os.ModePerm)) 767 } else { 768 require.NoError(t, os.WriteFile(path.Join(sourceInnodbLogDir, "innodb_log_1"), []byte("innodb log 1 contents"), os.ModePerm)) 769 } 770 771 require.NoError(t, os.WriteFile(path.Join(sourceDataDbDir, "db.opt"), []byte("db opt file"), os.ModePerm)) 772 773 // create a primary tablet, set its primary position 774 primary := NewFakeTablet(t, wr, "cell1", 0, topodatapb.TabletType_PRIMARY, db) 775 primary.FakeMysqlDaemon.ReadOnly = false 776 primary.FakeMysqlDaemon.Replicating = false 777 primary.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 778 GTIDSet: mysql.MariadbGTIDSet{ 779 2: mysql.MariadbGTID{ 780 Domain: 2, 781 Server: 123, 782 Sequence: 457, 783 }, 784 }, 785 } 786 787 // start primary so that replica can fetch primary position from it 788 primary.StartActionLoop(t, wr) 789 defer primary.StopActionLoop(t) 790 791 // create a single tablet, set it up so we can do backups 792 // set its position same as that of primary so that backup doesn't wait for catchup 793 sourceTablet := NewFakeTablet(t, wr, "cell1", 1, topodatapb.TabletType_REPLICA, db) 794 sourceTablet.FakeMysqlDaemon.ReadOnly = true 795 sourceTablet.FakeMysqlDaemon.Replicating = true 796 sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 797 GTIDSet: mysql.MariadbGTIDSet{ 798 2: mysql.MariadbGTID{ 799 Domain: 2, 800 Server: 123, 801 Sequence: 457, 802 }, 803 }, 804 } 805 sourceTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 806 "STOP SLAVE", 807 } 808 sourceTablet.StartActionLoop(t, wr) 809 defer sourceTablet.StopActionLoop(t) 810 811 sourceTablet.TM.Cnf = &mysqlctl.Mycnf{ 812 DataDir: sourceDataDir, 813 InnodbDataHomeDir: sourceInnodbDataDir, 814 InnodbLogGroupHomeDir: sourceInnodbLogDir, 815 } 816 817 // run the backup 818 require.NoError(t, vp.Run([]string{"Backup", topoproto.TabletAliasString(sourceTablet.Tablet.Alias)})) 819 820 // verify the full status 821 require.NoError(t, sourceTablet.FakeMysqlDaemon.CheckSuperQueryList()) 822 assert.False(t, sourceTablet.FakeMysqlDaemon.Replicating) 823 assert.True(t, sourceTablet.FakeMysqlDaemon.Running) 824 825 // create a destination tablet, set it up so we can do restores 826 destTablet := NewFakeTablet(t, wr, "cell1", 2, topodatapb.TabletType_REPLICA, db) 827 destTablet.FakeMysqlDaemon.ReadOnly = true 828 destTablet.FakeMysqlDaemon.Replicating = true 829 destTablet.FakeMysqlDaemon.CurrentPrimaryPosition = mysql.Position{ 830 GTIDSet: mysql.MariadbGTIDSet{ 831 2: mysql.MariadbGTID{ 832 Domain: 2, 833 Server: 123, 834 Sequence: 457, 835 }, 836 }, 837 } 838 destTablet.FakeMysqlDaemon.ExpectedExecuteSuperQueryList = []string{ 839 "STOP SLAVE", 840 "RESET SLAVE ALL", 841 "FAKE SET SLAVE POSITION", 842 } 843 destTablet.FakeMysqlDaemon.FetchSuperQueryMap = map[string]*sqltypes.Result{ 844 "SHOW DATABASES": {}, 845 } 846 destTablet.FakeMysqlDaemon.SetReplicationPositionPos = sourceTablet.FakeMysqlDaemon.CurrentPrimaryPosition 847 destTablet.FakeMysqlDaemon.SetReplicationSourceInputs = append(destTablet.FakeMysqlDaemon.SetReplicationSourceInputs, topoproto.MysqlAddr(primary.Tablet)) 848 849 destTablet.StartActionLoop(t, wr) 850 defer destTablet.StopActionLoop(t) 851 852 destTablet.TM.Cnf = &mysqlctl.Mycnf{ 853 DataDir: sourceDataDir, 854 InnodbDataHomeDir: sourceInnodbDataDir, 855 InnodbLogGroupHomeDir: sourceInnodbLogDir, 856 BinLogPath: path.Join(root, "bin-logs/filename_prefix"), 857 RelayLogPath: path.Join(root, "relay-logs/filename_prefix"), 858 RelayLogIndexPath: path.Join(root, "relay-log.index"), 859 RelayLogInfoPath: path.Join(root, "relay-log.info"), 860 } 861 862 require.NoError(t, destTablet.TM.RestoreData(ctx, logutil.NewConsoleLogger(), 0 /* waitForBackupInterval */, false /* deleteBeforeRestore */, time.Time{} /* restoreFromBackupTs */)) 863 // verify the full status 864 require.NoError(t, destTablet.FakeMysqlDaemon.CheckSuperQueryList(), "destTablet.FakeMysqlDaemon.CheckSuperQueryList failed") 865 assert.False(t, destTablet.FakeMysqlDaemon.Replicating) 866 assert.True(t, destTablet.FakeMysqlDaemon.Running) 867 } 868 869 // needInnoDBRedoLogSubdir indicates whether we need to create a redo log subdirectory. 870 // Starting with MySQL 8.0.30, the InnoDB redo logs are stored in a subdirectory of the 871 // <innodb_log_group_home_dir> (<datadir>/. by default) called "#innodb_redo". See: 872 // 873 // https://dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html#innodb-modifying-redo-log-capacity 874 func needInnoDBRedoLogSubdir() (needIt bool, err error) { 875 mysqldVersionStr, err := mysqlctl.GetVersionString() 876 if err != nil { 877 return needIt, err 878 } 879 _, sv, err := mysqlctl.ParseVersionString(mysqldVersionStr) 880 if err != nil { 881 return needIt, err 882 } 883 versionStr := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch) 884 _, capableOf, _ := mysql.GetFlavor(versionStr, nil) 885 if capableOf == nil { 886 return needIt, fmt.Errorf("cannot determine database flavor details for version %s", versionStr) 887 } 888 return capableOf(mysql.DynamicRedoLogCapacityFlavorCapability) 889 }