vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletmanager/rpc_replication.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 tabletmanager 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/spf13/pflag" 27 28 "vitess.io/vitess/go/mysql" 29 "vitess.io/vitess/go/vt/log" 30 "vitess.io/vitess/go/vt/logutil" 31 "vitess.io/vitess/go/vt/mysqlctl" 32 "vitess.io/vitess/go/vt/proto/vtrpc" 33 "vitess.io/vitess/go/vt/servenv" 34 "vitess.io/vitess/go/vt/topo/topoproto" 35 "vitess.io/vitess/go/vt/vterrors" 36 37 replicationdatapb "vitess.io/vitess/go/vt/proto/replicationdata" 38 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 39 ) 40 41 var setSuperReadOnly bool 42 var disableReplicationManager bool 43 44 func registerReplicationFlags(fs *pflag.FlagSet) { 45 fs.BoolVar(&setSuperReadOnly, "use_super_read_only", setSuperReadOnly, "Set super_read_only flag when performing planned failover.") 46 fs.BoolVar(&disableReplicationManager, "disable-replication-manager", disableReplicationManager, "Disable replication manager to prevent replication repairs.") 47 fs.MarkDeprecated("disable-replication-manager", "Replication manager is deleted") 48 } 49 50 func init() { 51 servenv.OnParseFor("vtcombo", registerReplicationFlags) 52 servenv.OnParseFor("vttablet", registerReplicationFlags) 53 } 54 55 // ReplicationStatus returns the replication status 56 func (tm *TabletManager) ReplicationStatus(ctx context.Context) (*replicationdatapb.Status, error) { 57 status, err := tm.MysqlDaemon.ReplicationStatus() 58 if err != nil { 59 return nil, err 60 } 61 return mysql.ReplicationStatusToProto(status), nil 62 } 63 64 // FullStatus returns the full status of MySQL including the replication information, semi-sync information, GTID information among others 65 func (tm *TabletManager) FullStatus(ctx context.Context) (*replicationdatapb.FullStatus, error) { 66 // Server ID - "select @@global.server_id" 67 serverID, err := tm.MysqlDaemon.GetServerID(ctx) 68 if err != nil { 69 return nil, err 70 } 71 72 // Server UUID - "select @@global.server_uuid" 73 serverUUID, err := tm.MysqlDaemon.GetServerUUID(ctx) 74 if err != nil { 75 return nil, err 76 } 77 78 // Replication status - "SHOW REPLICA STATUS" 79 replicationStatus, err := tm.MysqlDaemon.ReplicationStatus() 80 var replicationStatusProto *replicationdatapb.Status 81 if err != nil && err != mysql.ErrNotReplica { 82 return nil, err 83 } 84 if err == nil { 85 replicationStatusProto = mysql.ReplicationStatusToProto(replicationStatus) 86 } 87 88 // Primary status - "SHOW MASTER STATUS" 89 primaryStatus, err := tm.MysqlDaemon.PrimaryStatus(ctx) 90 var primaryStatusProto *replicationdatapb.PrimaryStatus 91 if err != nil && err != mysql.ErrNoPrimaryStatus { 92 return nil, err 93 } 94 if err == nil { 95 primaryStatusProto = mysql.PrimaryStatusToProto(primaryStatus) 96 } 97 98 // Purged GTID set 99 purgedGTIDs, err := tm.MysqlDaemon.GetGTIDPurged(ctx) 100 if err != nil { 101 return nil, err 102 } 103 104 // Version string "majorVersion.minorVersion.patchRelease" 105 version := tm.MysqlDaemon.GetVersionString() 106 107 // Version comment "select @@global.version_comment" 108 versionComment := tm.MysqlDaemon.GetVersionComment(ctx) 109 110 // Read only - "SHOW VARIABLES LIKE 'read_only'" 111 readOnly, err := tm.MysqlDaemon.IsReadOnly() 112 if err != nil { 113 return nil, err 114 } 115 116 // Binlog Information - "select @@global.binlog_format, @@global.log_bin, @@global.log_slave_updates, @@global.binlog_row_image" 117 binlogFormat, logBin, logReplicaUpdates, binlogRowImage, err := tm.MysqlDaemon.GetBinlogInformation(ctx) 118 if err != nil { 119 return nil, err 120 } 121 122 // GTID Mode - "select @@global.gtid_mode" - Only applicable for MySQL variants 123 gtidMode, err := tm.MysqlDaemon.GetGTIDMode(ctx) 124 if err != nil { 125 return nil, err 126 } 127 128 // Semi sync settings - "show global variables like 'rpl_semi_sync_%_enabled'" 129 primarySemiSync, replicaSemiSync := tm.MysqlDaemon.SemiSyncEnabled() 130 131 // Semi sync status - "show status like 'Rpl_semi_sync_%_status'" 132 primarySemiSyncStatus, replicaSemiSyncStatus := tm.MysqlDaemon.SemiSyncStatus() 133 134 // Semi sync clients count - "show status like 'semi_sync_primary_clients'" 135 semiSyncClients := tm.MysqlDaemon.SemiSyncClients() 136 137 // Semi sync settings - "show status like 'rpl_semi_sync_%' 138 semiSyncTimeout, semiSyncNumReplicas := tm.MysqlDaemon.SemiSyncSettings() 139 140 return &replicationdatapb.FullStatus{ 141 ServerId: serverID, 142 ServerUuid: serverUUID, 143 ReplicationStatus: replicationStatusProto, 144 PrimaryStatus: primaryStatusProto, 145 GtidPurged: mysql.EncodePosition(purgedGTIDs), 146 Version: version, 147 VersionComment: versionComment, 148 ReadOnly: readOnly, 149 GtidMode: gtidMode, 150 BinlogFormat: binlogFormat, 151 BinlogRowImage: binlogRowImage, 152 LogBinEnabled: logBin, 153 LogReplicaUpdates: logReplicaUpdates, 154 SemiSyncPrimaryEnabled: primarySemiSync, 155 SemiSyncReplicaEnabled: replicaSemiSync, 156 SemiSyncPrimaryStatus: primarySemiSyncStatus, 157 SemiSyncReplicaStatus: replicaSemiSyncStatus, 158 SemiSyncPrimaryClients: semiSyncClients, 159 SemiSyncPrimaryTimeout: semiSyncTimeout, 160 SemiSyncWaitForReplicaCount: semiSyncNumReplicas, 161 }, nil 162 } 163 164 // PrimaryStatus returns the replication status for a primary tablet. 165 func (tm *TabletManager) PrimaryStatus(ctx context.Context) (*replicationdatapb.PrimaryStatus, error) { 166 status, err := tm.MysqlDaemon.PrimaryStatus(ctx) 167 if err != nil { 168 return nil, err 169 } 170 return mysql.PrimaryStatusToProto(status), nil 171 } 172 173 // PrimaryPosition returns the position of a primary database 174 func (tm *TabletManager) PrimaryPosition(ctx context.Context) (string, error) { 175 pos, err := tm.MysqlDaemon.PrimaryPosition() 176 if err != nil { 177 return "", err 178 } 179 return mysql.EncodePosition(pos), nil 180 } 181 182 // WaitForPosition waits until replication reaches the desired position 183 func (tm *TabletManager) WaitForPosition(ctx context.Context, pos string) error { 184 log.Infof("WaitForPosition: %v", pos) 185 mpos, err := mysql.DecodePosition(pos) 186 if err != nil { 187 return err 188 } 189 return tm.MysqlDaemon.WaitSourcePos(ctx, mpos) 190 } 191 192 // StopReplication will stop the mysql. Works both when Vitess manages 193 // replication or not (using hook if not). 194 func (tm *TabletManager) StopReplication(ctx context.Context) error { 195 log.Infof("StopReplication") 196 if err := tm.lock(ctx); err != nil { 197 return err 198 } 199 defer tm.unlock() 200 201 return tm.stopReplicationLocked(ctx) 202 } 203 204 func (tm *TabletManager) stopReplicationLocked(ctx context.Context) error { 205 return tm.MysqlDaemon.StopReplication(tm.hookExtraEnv()) 206 } 207 208 func (tm *TabletManager) stopIOThreadLocked(ctx context.Context) error { 209 return tm.MysqlDaemon.StopIOThread(ctx) 210 } 211 212 // StopReplicationMinimum will stop the replication after it reaches at least the 213 // provided position. Works both when Vitess manages 214 // replication or not (using hook if not). 215 func (tm *TabletManager) StopReplicationMinimum(ctx context.Context, position string, waitTime time.Duration) (string, error) { 216 log.Infof("StopReplicationMinimum: position: %v waitTime: %v", position, waitTime) 217 if err := tm.lock(ctx); err != nil { 218 return "", err 219 } 220 defer tm.unlock() 221 222 pos, err := mysql.DecodePosition(position) 223 if err != nil { 224 return "", err 225 } 226 waitCtx, cancel := context.WithTimeout(ctx, waitTime) 227 defer cancel() 228 if err := tm.MysqlDaemon.WaitSourcePos(waitCtx, pos); err != nil { 229 return "", err 230 } 231 if err := tm.stopReplicationLocked(ctx); err != nil { 232 return "", err 233 } 234 pos, err = tm.MysqlDaemon.PrimaryPosition() 235 if err != nil { 236 return "", err 237 } 238 return mysql.EncodePosition(pos), nil 239 } 240 241 // StartReplication will start the mysql. Works both when Vitess manages 242 // replication or not (using hook if not). 243 func (tm *TabletManager) StartReplication(ctx context.Context, semiSync bool) error { 244 log.Infof("StartReplication") 245 if err := tm.lock(ctx); err != nil { 246 return err 247 } 248 defer tm.unlock() 249 250 if err := tm.fixSemiSync(tm.Tablet().Type, convertBoolToSemiSyncAction(semiSync)); err != nil { 251 return err 252 } 253 return tm.MysqlDaemon.StartReplication(tm.hookExtraEnv()) 254 } 255 256 // StartReplicationUntilAfter will start the replication and let it catch up 257 // until and including the transactions in `position` 258 func (tm *TabletManager) StartReplicationUntilAfter(ctx context.Context, position string, waitTime time.Duration) error { 259 log.Infof("StartReplicationUntilAfter: position: %v waitTime: %v", position, waitTime) 260 if err := tm.lock(ctx); err != nil { 261 return err 262 } 263 defer tm.unlock() 264 265 waitCtx, cancel := context.WithTimeout(ctx, waitTime) 266 defer cancel() 267 268 pos, err := mysql.DecodePosition(position) 269 if err != nil { 270 return err 271 } 272 273 return tm.MysqlDaemon.StartReplicationUntilAfter(waitCtx, pos) 274 } 275 276 // GetReplicas returns the address of all the replicas 277 func (tm *TabletManager) GetReplicas(ctx context.Context) ([]string, error) { 278 return mysqlctl.FindReplicas(tm.MysqlDaemon) 279 } 280 281 // ResetReplication completely resets the replication on the host. 282 // All binary and relay logs are flushed. All replication positions are reset. 283 func (tm *TabletManager) ResetReplication(ctx context.Context) error { 284 log.Infof("ResetReplication") 285 if err := tm.lock(ctx); err != nil { 286 return err 287 } 288 defer tm.unlock() 289 290 return tm.MysqlDaemon.ResetReplication(ctx) 291 } 292 293 // InitPrimary enables writes and returns the replication position. 294 func (tm *TabletManager) InitPrimary(ctx context.Context, semiSync bool) (string, error) { 295 log.Infof("InitPrimary with semiSync as %t", semiSync) 296 if err := tm.lock(ctx); err != nil { 297 return "", err 298 } 299 defer tm.unlock() 300 301 if setSuperReadOnly { 302 // Setting super_read_only off so that we can run the DDL commands 303 if err := tm.MysqlDaemon.SetSuperReadOnly(false); err != nil { 304 if strings.Contains(err.Error(), strconv.Itoa(mysql.ERUnknownSystemVariable)) { 305 log.Warningf("server does not know about super_read_only, continuing anyway...") 306 } else { 307 return "", err 308 } 309 } 310 } 311 312 // we need to generate a binlog entry so that we can get the current position. 313 cmd := mysqlctl.GenerateInitialBinlogEntry() 314 if err := tm.MysqlDaemon.ExecuteSuperQueryList(ctx, []string{cmd}); err != nil { 315 return "", err 316 } 317 318 // get the current replication position 319 pos, err := tm.MysqlDaemon.PrimaryPosition() 320 if err != nil { 321 return "", err 322 } 323 324 // Set the server read-write, from now on we can accept real 325 // client writes. Note that if semi-sync replication is enabled, 326 // we'll still need some replicas to be able to commit transactions. 327 if err := tm.changeTypeLocked(ctx, topodatapb.TabletType_PRIMARY, DBActionSetReadWrite, convertBoolToSemiSyncAction(semiSync)); err != nil { 328 return "", err 329 } 330 331 // Enforce semi-sync after changing the tablet type to PRIMARY. Otherwise, the 332 // primary will hang while trying to create the database. 333 if err := tm.fixSemiSync(topodatapb.TabletType_PRIMARY, convertBoolToSemiSyncAction(semiSync)); err != nil { 334 return "", err 335 } 336 337 return mysql.EncodePosition(pos), nil 338 } 339 340 // PopulateReparentJournal adds an entry into the reparent_journal table. 341 func (tm *TabletManager) PopulateReparentJournal(ctx context.Context, timeCreatedNS int64, actionName string, primaryAlias *topodatapb.TabletAlias, position string) error { 342 log.Infof("PopulateReparentJournal: action: %v parent: %v position: %v timeCreatedNS: %d actionName: %s primaryAlias: %s", 343 actionName, primaryAlias, position, timeCreatedNS, actionName, primaryAlias) 344 pos, err := mysql.DecodePosition(position) 345 if err != nil { 346 return err 347 } 348 349 cmds := []string{mysqlctl.PopulateReparentJournal(timeCreatedNS, actionName, topoproto.TabletAliasString(primaryAlias), pos)} 350 351 return tm.MysqlDaemon.ExecuteSuperQueryList(ctx, cmds) 352 } 353 354 // InitReplica sets replication primary and position, and waits for the 355 // reparent_journal table entry up to context timeout 356 func (tm *TabletManager) InitReplica(ctx context.Context, parent *topodatapb.TabletAlias, position string, timeCreatedNS int64, semiSync bool) error { 357 log.Infof("InitReplica: parent: %v position: %v timeCreatedNS: %d semisync: %t", parent, position, timeCreatedNS, semiSync) 358 if err := tm.lock(ctx); err != nil { 359 return err 360 } 361 defer tm.unlock() 362 363 // If we were a primary type, switch our type to replica. This 364 // is used on the old primary when using InitShardPrimary with 365 // -force, and the new primary is different from the old primary. 366 if tm.Tablet().Type == topodatapb.TabletType_PRIMARY { 367 if err := tm.changeTypeLocked(ctx, topodatapb.TabletType_REPLICA, DBActionNone, convertBoolToSemiSyncAction(semiSync)); err != nil { 368 return err 369 } 370 } 371 372 pos, err := mysql.DecodePosition(position) 373 if err != nil { 374 return err 375 } 376 ti, err := tm.TopoServer.GetTablet(ctx, parent) 377 if err != nil { 378 return err 379 } 380 381 // If using semi-sync, we need to enable it before connecting to primary. 382 // If we were a primary type, we need to switch back to replica settings. 383 // Otherwise we won't be able to commit anything. 384 tt := tm.Tablet().Type 385 if tt == topodatapb.TabletType_PRIMARY { 386 tt = topodatapb.TabletType_REPLICA 387 } 388 if err := tm.fixSemiSync(tt, convertBoolToSemiSyncAction(semiSync)); err != nil { 389 return err 390 } 391 392 if err := tm.MysqlDaemon.SetReplicationPosition(ctx, pos); err != nil { 393 return err 394 } 395 if err := tm.MysqlDaemon.SetReplicationSource(ctx, ti.Tablet.MysqlHostname, int(ti.Tablet.MysqlPort), false /* stopReplicationBefore */, true /* startReplicationAfter */); err != nil { 396 return err 397 } 398 399 // wait until we get the replicated row, or our context times out 400 return tm.MysqlDaemon.WaitForReparentJournal(ctx, timeCreatedNS) 401 } 402 403 // DemotePrimary prepares a PRIMARY tablet to give up leadership to another tablet. 404 // 405 // It attemps to idempotently ensure the following guarantees upon returning 406 // successfully: 407 // - No future writes will be accepted. 408 // - No writes are in-flight. 409 // - MySQL is in read-only mode. 410 // - Semi-sync settings are consistent with a REPLICA tablet. 411 // 412 // If necessary, it waits for all in-flight writes to complete or time out. 413 // 414 // It should be safe to call this on a PRIMARY tablet that was already demoted, 415 // or on a tablet that already transitioned to REPLICA. 416 // 417 // If a step fails in the middle, it will try to undo any changes it made. 418 func (tm *TabletManager) DemotePrimary(ctx context.Context) (*replicationdatapb.PrimaryStatus, error) { 419 log.Infof("DemotePrimary") 420 // The public version always reverts on partial failure. 421 return tm.demotePrimary(ctx, true /* revertPartialFailure */) 422 } 423 424 // demotePrimary implements DemotePrimary with an additional, private option. 425 // 426 // If revertPartialFailure is true, and a step fails in the middle, it will try 427 // to undo any changes it made. 428 func (tm *TabletManager) demotePrimary(ctx context.Context, revertPartialFailure bool) (primaryStatus *replicationdatapb.PrimaryStatus, finalErr error) { 429 if err := tm.lock(ctx); err != nil { 430 return nil, err 431 } 432 defer tm.unlock() 433 434 tablet := tm.Tablet() 435 wasPrimary := tablet.Type == topodatapb.TabletType_PRIMARY 436 wasServing := tm.QueryServiceControl.IsServing() 437 wasReadOnly, err := tm.MysqlDaemon.IsReadOnly() 438 if err != nil { 439 return nil, err 440 } 441 442 // If we are a primary tablet and not yet read-only, stop accepting new 443 // queries and wait for in-flight queries to complete. If we are not primary, 444 // or if we are already read-only, there's no need to stop the queryservice 445 // in order to ensure the guarantee we are being asked to provide, which is 446 // that no writes are occurring. 447 if wasPrimary && !wasReadOnly { 448 // Note that this may block until the transaction timeout if clients 449 // don't finish their transactions in time. Even if some transactions 450 // have to be killed at the end of their timeout, this will be 451 // considered successful. If we are already not serving, this will be 452 // idempotent. 453 log.Infof("DemotePrimary disabling query service") 454 if err := tm.QueryServiceControl.SetServingType(tablet.Type, logutil.ProtoToTime(tablet.PrimaryTermStartTime), false, "demotion in progress"); err != nil { 455 return nil, vterrors.Wrap(err, "SetServingType(serving=false) failed") 456 } 457 defer func() { 458 if finalErr != nil && revertPartialFailure && wasServing { 459 if err := tm.QueryServiceControl.SetServingType(tablet.Type, logutil.ProtoToTime(tablet.PrimaryTermStartTime), true, ""); err != nil { 460 log.Warningf("SetServingType(serving=true) failed during revert: %v", err) 461 } 462 } 463 }() 464 } 465 466 // Now that we know no writes are in-flight and no new writes can occur, 467 // set MySQL to read-only mode. If we are already read-only because of a 468 // previous demotion, or because we are not primary anyway, this should be 469 // idempotent. 470 if setSuperReadOnly { 471 // Setting super_read_only also sets read_only 472 if err := tm.MysqlDaemon.SetSuperReadOnly(true); err != nil { 473 if strings.Contains(err.Error(), strconv.Itoa(mysql.ERUnknownSystemVariable)) { 474 log.Warningf("server does not know about super_read_only, continuing anyway...") 475 } else { 476 return nil, err 477 } 478 } 479 } else { 480 if err := tm.MysqlDaemon.SetReadOnly(true); err != nil { 481 return nil, err 482 } 483 } 484 defer func() { 485 if finalErr != nil && revertPartialFailure && !wasReadOnly { 486 // setting read_only OFF will also set super_read_only OFF if it was set 487 if err := tm.MysqlDaemon.SetReadOnly(false); err != nil { 488 log.Warningf("SetReadOnly(false) failed during revert: %v", err) 489 } 490 } 491 }() 492 493 // Here, we check if the primary side semi sync is enabled or not. If it isn't enabled then we do not need to take any action. 494 // If it is enabled then we should turn it off and revert in case of failure. 495 if tm.isPrimarySideSemiSyncEnabled() { 496 // If using semi-sync, we need to disable primary-side. 497 if err := tm.fixSemiSync(topodatapb.TabletType_REPLICA, SemiSyncActionSet); err != nil { 498 return nil, err 499 } 500 defer func() { 501 if finalErr != nil && revertPartialFailure && wasPrimary { 502 // enable primary-side semi-sync again 503 if err := tm.fixSemiSync(topodatapb.TabletType_PRIMARY, SemiSyncActionSet); err != nil { 504 log.Warningf("fixSemiSync(PRIMARY) failed during revert: %v", err) 505 } 506 } 507 }() 508 } 509 510 // Return the current replication position. 511 status, err := tm.MysqlDaemon.PrimaryStatus(ctx) 512 if err != nil { 513 return nil, err 514 } 515 return mysql.PrimaryStatusToProto(status), nil 516 } 517 518 // UndoDemotePrimary reverts a previous call to DemotePrimary 519 // it sets read-only to false, fixes semi-sync 520 // and returns its primary position. 521 func (tm *TabletManager) UndoDemotePrimary(ctx context.Context, semiSync bool) error { 522 log.Infof("UndoDemotePrimary") 523 if err := tm.lock(ctx); err != nil { 524 return err 525 } 526 defer tm.unlock() 527 528 // If using semi-sync, we need to enable source-side. 529 if err := tm.fixSemiSync(topodatapb.TabletType_PRIMARY, convertBoolToSemiSyncAction(semiSync)); err != nil { 530 return err 531 } 532 533 // Now, set the server read-only false. 534 if err := tm.MysqlDaemon.SetReadOnly(false); err != nil { 535 return err 536 } 537 538 // Update serving graph 539 tablet := tm.Tablet() 540 log.Infof("UndoDemotePrimary re-enabling query service") 541 if err := tm.QueryServiceControl.SetServingType(tablet.Type, logutil.ProtoToTime(tablet.PrimaryTermStartTime), true, ""); err != nil { 542 return vterrors.Wrap(err, "SetServingType(serving=true) failed") 543 } 544 return nil 545 } 546 547 // ReplicaWasPromoted promotes a replica to primary, no questions asked. 548 func (tm *TabletManager) ReplicaWasPromoted(ctx context.Context) error { 549 log.Infof("ReplicaWasPromoted") 550 if err := tm.lock(ctx); err != nil { 551 return err 552 } 553 defer tm.unlock() 554 return tm.changeTypeLocked(ctx, topodatapb.TabletType_PRIMARY, DBActionNone, SemiSyncActionNone) 555 } 556 557 // ResetReplicationParameters resets the replica replication parameters 558 func (tm *TabletManager) ResetReplicationParameters(ctx context.Context) error { 559 log.Infof("ResetReplicationParameters") 560 if err := tm.lock(ctx); err != nil { 561 return err 562 } 563 defer tm.unlock() 564 565 err := tm.MysqlDaemon.StopReplication(tm.hookExtraEnv()) 566 if err != nil { 567 return err 568 } 569 570 err = tm.MysqlDaemon.ResetReplicationParameters(ctx) 571 if err != nil { 572 return err 573 } 574 return nil 575 } 576 577 // SetReplicationSource sets replication primary, and waits for the 578 // reparent_journal table entry up to context timeout 579 func (tm *TabletManager) SetReplicationSource(ctx context.Context, parentAlias *topodatapb.TabletAlias, timeCreatedNS int64, waitPosition string, forceStartReplication bool, semiSync bool) error { 580 log.Infof("SetReplicationSource: parent: %v position: %s force: %v semiSync: %v timeCreatedNS: %d", parentAlias, waitPosition, forceStartReplication, semiSync, timeCreatedNS) 581 if err := tm.lock(ctx); err != nil { 582 return err 583 } 584 defer tm.unlock() 585 586 // setReplicationSourceLocked also fixes the semi-sync. In case the tablet type is primary it assumes that it will become a replica if SetReplicationSource 587 // is called, so we always call fixSemiSync with a non-primary tablet type. This will always set the source side replication to false. 588 return tm.setReplicationSourceLocked(ctx, parentAlias, timeCreatedNS, waitPosition, forceStartReplication, convertBoolToSemiSyncAction(semiSync)) 589 } 590 591 func (tm *TabletManager) setReplicationSourceRepairReplication(ctx context.Context, parentAlias *topodatapb.TabletAlias, timeCreatedNS int64, waitPosition string, forceStartReplication bool) (err error) { 592 parent, err := tm.TopoServer.GetTablet(ctx, parentAlias) 593 if err != nil { 594 return err 595 } 596 597 ctx, unlock, lockErr := tm.TopoServer.LockShard(ctx, parent.Tablet.GetKeyspace(), parent.Tablet.GetShard(), fmt.Sprintf("repairReplication to %v as parent)", topoproto.TabletAliasString(parentAlias))) 598 if lockErr != nil { 599 return lockErr 600 } 601 602 defer unlock(&err) 603 604 return tm.setReplicationSourceLocked(ctx, parentAlias, timeCreatedNS, waitPosition, forceStartReplication, SemiSyncActionNone) 605 } 606 607 func (tm *TabletManager) setReplicationSourceSemiSyncNoAction(ctx context.Context, parentAlias *topodatapb.TabletAlias, timeCreatedNS int64, waitPosition string, forceStartReplication bool) error { 608 log.Infof("SetReplicationSource: parent: %v position: %v force: %v", parentAlias, waitPosition, forceStartReplication) 609 if err := tm.lock(ctx); err != nil { 610 return err 611 } 612 defer tm.unlock() 613 614 return tm.setReplicationSourceLocked(ctx, parentAlias, timeCreatedNS, waitPosition, forceStartReplication, SemiSyncActionNone) 615 } 616 617 func (tm *TabletManager) setReplicationSourceLocked(ctx context.Context, parentAlias *topodatapb.TabletAlias, timeCreatedNS int64, waitPosition string, forceStartReplication bool, semiSync SemiSyncAction) (err error) { 618 // Change our type to REPLICA if we used to be PRIMARY. 619 // Being sent SetReplicationSource means another PRIMARY has been successfully promoted, 620 // so we convert to REPLICA first, since we want to do it even if other 621 // steps fail below. 622 // Note it is important to check for PRIMARY here so that we don't 623 // unintentionally change the type of RDONLY tablets 624 tablet := tm.Tablet() 625 if tablet.Type == topodatapb.TabletType_PRIMARY { 626 if err := tm.tmState.ChangeTabletType(ctx, topodatapb.TabletType_REPLICA, DBActionNone); err != nil { 627 return err 628 } 629 } 630 631 // See if we were replicating at all, and should be replicating. 632 wasReplicating := false 633 shouldbeReplicating := false 634 status, err := tm.MysqlDaemon.ReplicationStatus() 635 if err == mysql.ErrNotReplica { 636 // This is a special error that means we actually succeeded in reading 637 // the status, but the status is empty because replication is not 638 // configured. We assume this means we used to be a primary, so we always 639 // try to start replicating once we are told who the new primary is. 640 shouldbeReplicating = true 641 // Since we continue in the case of this error, make sure 'status' is 642 // in a known, empty state. 643 status = mysql.ReplicationStatus{} 644 } else if err != nil { 645 // Abort on any other non-nil error. 646 return err 647 } 648 if status.IOHealthy() || status.SQLHealthy() { 649 wasReplicating = true 650 shouldbeReplicating = true 651 } 652 if forceStartReplication { 653 shouldbeReplicating = true 654 } 655 656 // If using semi-sync, we need to enable it before connecting to primary. 657 // If we are currently PRIMARY, assume we are about to become REPLICA. 658 tabletType := tm.Tablet().Type 659 if tabletType == topodatapb.TabletType_PRIMARY { 660 tabletType = topodatapb.TabletType_REPLICA 661 } 662 if err := tm.fixSemiSync(tabletType, semiSync); err != nil { 663 return err 664 } 665 // Update the primary/source address only if needed. 666 // We don't want to interrupt replication for no reason. 667 if parentAlias == nil { 668 // if there is no primary in the shard, return an error so that we can retry 669 return vterrors.New(vtrpc.Code_FAILED_PRECONDITION, "Shard primaryAlias is nil") 670 } 671 parent, err := tm.TopoServer.GetTablet(ctx, parentAlias) 672 if err != nil { 673 return err 674 } 675 host := parent.Tablet.MysqlHostname 676 port := int(parent.Tablet.MysqlPort) 677 // We want to reset the replication parameters and set replication source again when forceStartReplication is provided 678 // because sometimes MySQL gets stuck due to improper initialization of master info structure or related failures and throws errors like 679 // ERROR 1201 (HY000): Could not initialize master info structure; more error messages can be found in the MySQL error log 680 // These errors can only be resolved by resetting the replication parameters, otherwise START SLAVE fails. So when this RPC 681 // gets called from VTOrc or replication manager to fix the replication in these cases with forceStartReplication, we should also 682 // reset the replication parameters and set the source port information again. 683 if status.SourceHost != host || status.SourcePort != port || forceStartReplication { 684 // This handles reseting the replication parameters, changing the address and then starting the replication. 685 if err := tm.MysqlDaemon.SetReplicationSource(ctx, host, port, wasReplicating, shouldbeReplicating); err != nil { 686 if err := tm.handleRelayLogError(err); err != nil { 687 return err 688 } 689 } 690 } else if shouldbeReplicating { 691 // The address is correct. We need to restart replication so that any semi-sync changes if any 692 // are taken into account 693 if err := tm.MysqlDaemon.StopReplication(tm.hookExtraEnv()); err != nil { 694 if err := tm.handleRelayLogError(err); err != nil { 695 return err 696 } 697 } 698 if err := tm.MysqlDaemon.StartReplication(tm.hookExtraEnv()); err != nil { 699 if err := tm.handleRelayLogError(err); err != nil { 700 return err 701 } 702 } 703 } 704 705 // If needed, wait until we replicate to the specified point, or our context 706 // times out. Callers can specify the point to wait for as either a 707 // GTID-based replication position or a Vitess reparent journal entry, 708 // or both. 709 if shouldbeReplicating { 710 log.Infof("Set up MySQL replication; should now be replicating from %s at %s", parentAlias, waitPosition) 711 if waitPosition != "" { 712 pos, err := mysql.DecodePosition(waitPosition) 713 if err != nil { 714 return err 715 } 716 if err := tm.MysqlDaemon.WaitSourcePos(ctx, pos); err != nil { 717 return err 718 } 719 } 720 if timeCreatedNS != 0 { 721 if err := tm.MysqlDaemon.WaitForReparentJournal(ctx, timeCreatedNS); err != nil { 722 return err 723 } 724 } 725 } 726 727 return nil 728 } 729 730 // ReplicaWasRestarted updates the parent record for a tablet. 731 func (tm *TabletManager) ReplicaWasRestarted(ctx context.Context, parent *topodatapb.TabletAlias) error { 732 log.Infof("ReplicaWasRestarted: parent: %v", parent) 733 if err := tm.lock(ctx); err != nil { 734 return err 735 } 736 defer tm.unlock() 737 738 // Only change type of former PRIMARY tablets. 739 // Don't change type of RDONLY 740 tablet := tm.Tablet() 741 if tablet.Type != topodatapb.TabletType_PRIMARY { 742 return nil 743 } 744 return tm.tmState.ChangeTabletType(ctx, topodatapb.TabletType_REPLICA, DBActionNone) 745 } 746 747 // StopReplicationAndGetStatus stops MySQL replication, and returns the 748 // current status. 749 func (tm *TabletManager) StopReplicationAndGetStatus(ctx context.Context, stopReplicationMode replicationdatapb.StopReplicationMode) (StopReplicationAndGetStatusResponse, error) { 750 log.Infof("StopReplicationAndGetStatus: mode: %v", stopReplicationMode) 751 if err := tm.lock(ctx); err != nil { 752 return StopReplicationAndGetStatusResponse{}, err 753 } 754 defer tm.unlock() 755 756 // Get the status before we stop replication. 757 // Doing this first allows us to return the status in the case that stopping replication 758 // returns an error, so a user can optionally inspect the status before a stop was called. 759 rs, err := tm.MysqlDaemon.ReplicationStatus() 760 if err != nil { 761 return StopReplicationAndGetStatusResponse{}, vterrors.Wrap(err, "before status failed") 762 } 763 before := mysql.ReplicationStatusToProto(rs) 764 765 if stopReplicationMode == replicationdatapb.StopReplicationMode_IOTHREADONLY { 766 if !rs.IOHealthy() { 767 return StopReplicationAndGetStatusResponse{ 768 Status: &replicationdatapb.StopReplicationStatus{ 769 Before: before, 770 After: before, 771 }, 772 }, nil 773 } 774 if err := tm.stopIOThreadLocked(ctx); err != nil { 775 return StopReplicationAndGetStatusResponse{ 776 Status: &replicationdatapb.StopReplicationStatus{ 777 Before: before, 778 }, 779 }, vterrors.Wrap(err, "stop io thread failed") 780 } 781 } else { 782 if !rs.Healthy() { 783 // no replication is running, just return what we got 784 return StopReplicationAndGetStatusResponse{ 785 Status: &replicationdatapb.StopReplicationStatus{ 786 Before: before, 787 After: before, 788 }, 789 }, nil 790 } 791 if err := tm.stopReplicationLocked(ctx); err != nil { 792 return StopReplicationAndGetStatusResponse{ 793 Status: &replicationdatapb.StopReplicationStatus{ 794 Before: before, 795 }, 796 }, vterrors.Wrap(err, "stop replication failed") 797 } 798 } 799 800 // Get the status after we stop replication so we have up to date position and relay log positions. 801 rsAfter, err := tm.MysqlDaemon.ReplicationStatus() 802 if err != nil { 803 return StopReplicationAndGetStatusResponse{ 804 Status: &replicationdatapb.StopReplicationStatus{ 805 Before: before, 806 }, 807 }, vterrors.Wrap(err, "acquiring replication status failed") 808 } 809 after := mysql.ReplicationStatusToProto(rsAfter) 810 811 rs.Position = rsAfter.Position 812 rs.RelayLogPosition = rsAfter.RelayLogPosition 813 rs.FilePosition = rsAfter.FilePosition 814 rs.RelayLogSourceBinlogEquivalentPosition = rsAfter.RelayLogSourceBinlogEquivalentPosition 815 816 return StopReplicationAndGetStatusResponse{ 817 Status: &replicationdatapb.StopReplicationStatus{ 818 Before: before, 819 After: after, 820 }, 821 }, nil 822 } 823 824 // StopReplicationAndGetStatusResponse holds the original hybrid Status struct, as well as a new Status field, which 825 // hold the result of show replica status called before stopping replication, and after stopping replication. 826 type StopReplicationAndGetStatusResponse struct { 827 // Status represents the replication status call right before, and right after telling the replica to stop. 828 Status *replicationdatapb.StopReplicationStatus 829 } 830 831 // PromoteReplica makes the current tablet the primary 832 func (tm *TabletManager) PromoteReplica(ctx context.Context, semiSync bool) (string, error) { 833 log.Infof("PromoteReplica") 834 if err := tm.lock(ctx); err != nil { 835 return "", err 836 } 837 defer tm.unlock() 838 839 pos, err := tm.MysqlDaemon.Promote(tm.hookExtraEnv()) 840 if err != nil { 841 return "", err 842 } 843 844 // If using semi-sync, we need to enable it before going read-write. 845 if err := tm.fixSemiSync(topodatapb.TabletType_PRIMARY, convertBoolToSemiSyncAction(semiSync)); err != nil { 846 return "", err 847 } 848 849 if err := tm.changeTypeLocked(ctx, topodatapb.TabletType_PRIMARY, DBActionSetReadWrite, SemiSyncActionNone); err != nil { 850 return "", err 851 } 852 return mysql.EncodePosition(pos), nil 853 } 854 855 func isPrimaryEligible(tabletType topodatapb.TabletType) bool { 856 switch tabletType { 857 case topodatapb.TabletType_PRIMARY, topodatapb.TabletType_REPLICA: 858 return true 859 } 860 861 return false 862 } 863 864 func (tm *TabletManager) fixSemiSync(tabletType topodatapb.TabletType, semiSync SemiSyncAction) error { 865 switch semiSync { 866 case SemiSyncActionNone: 867 return nil 868 case SemiSyncActionSet: 869 // Always enable replica-side since it doesn't hurt to keep it on for a primary. 870 // The primary-side needs to be off for a replica, or else it will get stuck. 871 return tm.MysqlDaemon.SetSemiSyncEnabled(tabletType == topodatapb.TabletType_PRIMARY, true) 872 case SemiSyncActionUnset: 873 return tm.MysqlDaemon.SetSemiSyncEnabled(false, false) 874 default: 875 return vterrors.Errorf(vtrpc.Code_INTERNAL, "Unknown SemiSyncAction - %v", semiSync) 876 } 877 } 878 879 func (tm *TabletManager) isPrimarySideSemiSyncEnabled() bool { 880 semiSyncEnabled, _ := tm.MysqlDaemon.SemiSyncEnabled() 881 return semiSyncEnabled 882 } 883 884 func (tm *TabletManager) fixSemiSyncAndReplication(tabletType topodatapb.TabletType, semiSync SemiSyncAction) error { 885 if semiSync == SemiSyncActionNone { 886 // Semi-sync handling is not required. 887 return nil 888 } 889 890 if tabletType == topodatapb.TabletType_PRIMARY { 891 // Primary is special. It is always handled at the 892 // right time by the reparent operations, it doesn't 893 // need to be fixed. 894 return nil 895 } 896 897 if err := tm.fixSemiSync(tabletType, semiSync); err != nil { 898 return vterrors.Wrapf(err, "failed to fixSemiSync(%v)", tabletType) 899 } 900 901 // If replication is running, but the status is wrong, 902 // we should restart replication. First, let's make sure 903 // replication is running. 904 status, err := tm.MysqlDaemon.ReplicationStatus() 905 if err != nil { 906 // Replication is not configured, nothing to do. 907 return nil 908 } 909 if !status.IOHealthy() { 910 // IO thread is not running, nothing to do. 911 return nil 912 } 913 914 //shouldAck := semiSync == SemiSyncActionSet 915 shouldAck := isPrimaryEligible(tabletType) 916 acking, err := tm.MysqlDaemon.SemiSyncReplicationStatus() 917 if err != nil { 918 return vterrors.Wrap(err, "failed to get SemiSyncReplicationStatus") 919 } 920 if shouldAck == acking { 921 return nil 922 } 923 924 // We need to restart replication 925 log.Infof("Restarting replication for semi-sync flag change to take effect from %v to %v", acking, shouldAck) 926 if err := tm.MysqlDaemon.StopReplication(tm.hookExtraEnv()); err != nil { 927 return vterrors.Wrap(err, "failed to StopReplication") 928 } 929 if err := tm.MysqlDaemon.StartReplication(tm.hookExtraEnv()); err != nil { 930 return vterrors.Wrap(err, "failed to StartReplication") 931 } 932 return nil 933 } 934 935 func (tm *TabletManager) handleRelayLogError(err error) error { 936 // attempt to fix this error: 937 // Slave failed to initialize relay log info structure from the repository (errno 1872) (sqlstate HY000) during query: START SLAVE 938 // see https://bugs.mysql.com/bug.php?id=83713 or https://github.com/vitessio/vitess/issues/5067 939 if strings.Contains(err.Error(), "Slave failed to initialize relay log info structure from the repository") { 940 // Stop, reset and start replication again to resolve this error 941 if err := tm.MysqlDaemon.RestartReplication(tm.hookExtraEnv()); err != nil { 942 return err 943 } 944 return nil 945 } 946 return err 947 } 948 949 // repairReplication tries to connect this server to whoever is 950 // the current primary of the shard, and start replicating. 951 func (tm *TabletManager) repairReplication(ctx context.Context) error { 952 tablet := tm.Tablet() 953 954 si, err := tm.TopoServer.GetShard(ctx, tablet.Keyspace, tablet.Shard) 955 if err != nil { 956 return err 957 } 958 if !si.HasPrimary() { 959 return fmt.Errorf("no primary tablet for shard %v/%v", tablet.Keyspace, tablet.Shard) 960 } 961 962 if topoproto.TabletAliasEqual(si.PrimaryAlias, tablet.Alias) { 963 // The shard record says we are primary, but we disagree; we wouldn't 964 // reach this point unless we were told to check replication. 965 // Hopefully someone is working on fixing that, but in any case, 966 // we should not try to reparent to ourselves. 967 return fmt.Errorf("shard %v/%v record claims tablet %v is primary, but its type is %v", tablet.Keyspace, tablet.Shard, topoproto.TabletAliasString(tablet.Alias), tablet.Type) 968 } 969 return tm.setReplicationSourceRepairReplication(ctx, si.PrimaryAlias, 0, "", true) 970 }