Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba814b6
Add new `force` flag to `DemotePrimary` to force a failover during `E…
arthurschreiber Oct 2, 2025
8a54593
Merge branch 'main' of https://github.com/vitessio/vitess into arthur…
arthurschreiber Oct 15, 2025
0444df7
Unblock semi-sync by disabling it on the source.
arthurschreiber Oct 16, 2025
8adec1d
Fix behaviour when not forcing the demotion.
arthurschreiber Oct 16, 2025
bde16be
Add a test case.
arthurschreiber Oct 21, 2025
26aa0b4
Improve the test case.
arthurschreiber Oct 22, 2025
dda3b0a
Bump the timeouts.
arthurschreiber Oct 22, 2025
429964e
Skip this test on older vttablet versions.
arthurschreiber Oct 22, 2025
9740147
Merge remote-tracking branch 'origin/main' into arthur/add-forced-dem…
timvaillancourt Nov 3, 2025
7c5e650
linter fixes
timvaillancourt Nov 3, 2025
88b06ad
wait for stalled write in `TestEmergencyReparentWithBlockedPrimary`
timvaillancourt Nov 4, 2025
cb19d48
fix context
timvaillancourt Nov 4, 2025
1c91227
add suggested test
timvaillancourt Nov 4, 2025
33f4cbe
test insert blocks, check error from client
timvaillancourt Nov 5, 2025
691e337
remove static sleeps
timvaillancourt Nov 5, 2025
731198d
better comments, check vtgate stats
timvaillancourt Nov 5, 2025
76823d1
missing require
timvaillancourt Nov 5, 2025
2294c2b
validate tablet types
timvaillancourt Nov 5, 2025
feb665c
simplify
timvaillancourt Nov 11, 2025
b1722b9
lint
timvaillancourt Nov 11, 2025
f2622c4
test primary vttablet is really unreachable
timvaillancourt Nov 11, 2025
a805c4a
Revert "test primary vttablet is really unreachable"
timvaillancourt Nov 17, 2025
6284f7b
Add comments to explain the reasoning here.
arthurschreiber Nov 19, 2025
568d148
use strings.EqualFold
timvaillancourt Nov 19, 2025
4f6df49
Change this back to be always `true`.
arthurschreiber Nov 19, 2025
e00923a
Merge branch 'main' of https://github.com/vitessio/vitess into arthur…
arthurschreiber Nov 19, 2025
1f57d86
Fix test failures after merging latest changes.
arthurschreiber Nov 19, 2025
3a4c5a7
Merge branch 'main' of https://github.com/vitessio/vitess into arthur…
arthurschreiber Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions go/vt/mysqlctl/fakemysqldaemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,11 @@ func (fmd *FakeMysqlDaemon) SemiSyncReplicationStatus(ctx context.Context) (bool
return fmd.SemiSyncReplicaEnabled, nil
}

// IsSemiSyncBlocked is part of the MysqlDaemon interface.
func (fmd *FakeMysqlDaemon) IsSemiSyncBlocked(ctx context.Context) (bool, error) {
return false, nil
}

// GetVersionString is part of the MysqlDaemon interface.
func (fmd *FakeMysqlDaemon) GetVersionString(ctx context.Context) (string, error) {
return fmd.Version, nil
Expand Down
1 change: 1 addition & 0 deletions go/vt/mysqlctl/mysql_daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type MysqlDaemon interface {
SemiSyncClients(ctx context.Context) (count uint32)
SemiSyncSettings(ctx context.Context) (timeout uint64, numReplicas uint32)
SemiSyncReplicationStatus(ctx context.Context) (bool, error)
IsSemiSyncBlocked(ctx context.Context) (bool, error)
ResetReplicationParameters(ctx context.Context) error
GetBinlogInformation(ctx context.Context) (binlogFormat string, logEnabled bool, logReplicaUpdate bool, binlogRowImage string, err error)
GetGTIDMode(ctx context.Context) (gtidMode string, err error)
Expand Down
28 changes: 28 additions & 0 deletions go/vt/mysqlctl/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,31 @@ func (mysqld *Mysqld) SemiSyncExtensionLoaded(ctx context.Context) (mysql.SemiSy

return conn.Conn.SemiSyncExtensionLoaded()
}

func (mysqld *Mysqld) IsSemiSyncBlocked(ctx context.Context) (bool, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we have another method to check this when the monitor exposes methods for this as well.

conn, err := getPoolReconnect(ctx, mysqld.dbaPool)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not expect that we need to use the DBA pool for this as we're just selecting from P_S. The DBA pools tend to be relatively small and should only be used when truly necessary or they can block other things that are critical and do require it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered this too, but I don't know what alternatives we have

I have seen the small DBA pool get exhausted before, which prevents reparent RPCs that also use that pool. The root cause was likely overloaded mysqld or semi-sync issues, but filling that pool is a risk

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I used this pool because that's what all the other operations (like SetReplicationSource) do too:

func (mysqld *Mysqld) SetReplicationSource(ctx context.Context, host string, port int32, heartbeatInterval float64, stopReplicationBefore bool, startReplicationAfter bool) error {
params, err := mysqld.dbcfgs.ReplConnector().MysqlParams()
if err != nil {
return err
}
conn, err := getPoolReconnect(ctx, mysqld.dbaPool)
if err != nil {
return err
}
defer conn.Recycle()

Maybe it would be overall better to fetch a connection from the pool and then use that connection for all the operations that happen during commands like demotePrimary, but that's a bigger change that's outside of the scope of this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that's a bigger change that's outside of the scope of this PR.

Yeah, I don't think we should include that kind of change in this PR, but it's a risk

For some context: Slack has seen overloaded tablets cause this pool to fill with FullStatus (from VTOrc, gets worse with more instances) and this can prevent the actual RPCs that fix the problem to get denied outside the full pool

I think we'll just need to keep this in mind, and only 1 x RPC is being added to the pile here 👍/👎

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I don't think we should use DBA everywhere here, but it does line up with the other uses. And using a single connection for all of the work might be a good idea here too. Not related though so we can investigate this more separately.

if err != nil {
return false, err
}
defer conn.Recycle()

// Execute the query to check if the primary is blocked on semi-sync.
semiSyncWaitSessionsRead := "select variable_value from performance_schema.global_status where regexp_like(variable_name, 'Rpl_semi_sync_(source|master)_wait_sessions')"
res, err := conn.Conn.ExecuteFetch(semiSyncWaitSessionsRead, 1, false)
if err != nil {
return false, err
}
// If we have no rows, then the primary doesn't have semi-sync enabled.
// It then follows, that the primary isn't blocked :)
if len(res.Rows) == 0 {
return false, nil
}

// Read the status value and check if it is non-zero.
if len(res.Rows) != 1 || len(res.Rows[0]) != 1 {
return false, fmt.Errorf("unexpected number of rows received - %v", res.Rows)
}
value, err := res.Rows[0][0].ToCastInt64()
return value != 0, err

}
13 changes: 11 additions & 2 deletions go/vt/proto/tabletmanagerdata/tabletmanagerdata.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions go/vt/proto/tabletmanagerdata/tabletmanagerdata_vtproto.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/vt/vtcombo/tablet_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ func (itmc *internalTabletManagerClient) ReadReparentJournalInfo(ctx context.Con
return 0, fmt.Errorf("not implemented in vtcombo")
}

func (itmc *internalTabletManagerClient) DemotePrimary(context.Context, *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
func (itmc *internalTabletManagerClient) DemotePrimary(context.Context, *topodatapb.Tablet, bool) (*replicationdatapb.PrimaryStatus, error) {
return nil, fmt.Errorf("not implemented in vtcombo")
}

Expand Down
2 changes: 1 addition & 1 deletion go/vt/vtctl/grpcvtctldserver/testutil/test_tmclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ func (fake *TabletManagerClient) ChangeType(ctx context.Context, tablet *topodat
}

// DemotePrimary is part of the tmclient.TabletManagerClient interface.
func (fake *TabletManagerClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
func (fake *TabletManagerClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet, force bool) (*replicationdatapb.PrimaryStatus, error) {
if fake.DemotePrimaryResults == nil {
return nil, assert.AnError
}
Expand Down
4 changes: 2 additions & 2 deletions go/vt/vtctl/reparentutil/planned_reparenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func (pr *PlannedReparenter) performGracefulPromotion(
demoteCtx, demoteCancel := context.WithTimeout(ctx, topo.RemoteOperationTimeout)
defer demoteCancel()

primaryStatus, err := pr.tmc.DemotePrimary(demoteCtx, currentPrimary.Tablet)
primaryStatus, err := pr.tmc.DemotePrimary(demoteCtx, currentPrimary.Tablet, false)
if err != nil {
return vterrors.Wrapf(err, "failed to DemotePrimary on current primary %v: %v", currentPrimary.AliasString(), err)
}
Expand Down Expand Up @@ -426,7 +426,7 @@ func (pr *PlannedReparenter) performPotentialPromotion(
// tablet type), that's already in read-only.
pr.logger.Infof("demoting tablet %v", alias)

primaryStatus, err := pr.tmc.DemotePrimary(stopAllCtx, tablet)
primaryStatus, err := pr.tmc.DemotePrimary(stopAllCtx, tablet, false)
if err != nil {
rec.RecordError(vterrors.Wrapf(err, "DemotePrimary(%v) failed on contested primary", alias))

Expand Down
2 changes: 1 addition & 1 deletion go/vt/vtctl/reparentutil/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func stopReplicationAndBuildStatusMaps(
if isSQLErr && sqlErr != nil && sqlErr.Number() == sqlerror.ERNotReplica {
var primaryStatus *replicationdatapb.PrimaryStatus

primaryStatus, err = tmc.DemotePrimary(groupCtx, tabletInfo.Tablet)
primaryStatus, err = tmc.DemotePrimary(groupCtx, tabletInfo.Tablet, true /* force */)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, this function is only called from ERS today so makes sense. 👍

@timvaillancourt timvaillancourt Oct 30, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arthurschreiber I haven't thought of a specific reason or use case, etc but I wanted to point out that VTOrc could have a separate problem for a "stalled demoting primary", perhaps DeadPrimaryDemoting (hand-waving), and only this adds the force. Would this give us any value, even just in observability/debugging, or maybe safety? I don't see a benefit yet but thought I'd share. TL;DR: I'm wondering if we benefit from the force being optional in ERS 🤔

In the same category of "do we ever need force to be false in ERS?", I wanted to share/validate that this is safe for every use of ERS from VTOrc. VTOrc uses ERS to solve many problems, sometimes in unique ways like fixing a deleted topo record, so I wanted to be sure. I'll continue to think about a real-world scenario, for now I just wanted to bring it up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to point out that VTOrc could have a separate problem for a "stalled demoting primary", perhaps DeadPrimaryDemoting (hand-waving)

Appending here: to achieve this the FullStatus RPC would need to communicate to VTOrc that we're in a "stalled demoting" state. So that would require some more changes to achieve. Just an idea

@arthurschreiber arthurschreiber Oct 31, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timvaillancourt I don't necessarily see the value in that.

In my eyes, due to the way EmergencyReparentShard works, a stalled primary is simply an expected outcome, and it needs to be expected and handled when EmergencyReparentShard. Not handling this here but requiring the use of vtorc and requiring it to detect and then fix this situation seems suboptimal.

In the same category of "do we ever need force to be false in ERS?"

I'm still not sure whether enabling this always would be safe. Because writes that happen out of band of Vitess could be lost because we disable semi-sync before marking the server read-only. I guess the same can happen with the force flag though. 🤷

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my eyes, due to the way EmergencyReparentShard works, a stalled primary is simply an expected outcome, and it needs to be expected and handled when EmergencyReparentShard. Not handling this here but requiring the use of vtorc and requiring it to detect and then fix this situation seems suboptimal.

This kind of makes sense, but in my experience this doesn't happen "always" - or at least I don't think we had 100% of ERS result in a stuck-demoted PRIMARY at Slack. This scenario definitely has happened sometimes-to-often, but I don't think 100%

I wonder if @rvrangel / @tanjinx could validate this, or provide some data how often an ERS results in a tablet forever demoting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today, stopReplicationAndBuildStatusMaps appears to be used only for ERS. The function does not appear to be specific to ERS though. So IMO it would be better to make this explicitly tied to ERS. Meaning:

  1. This function takes a new bool for forceDemotion
  2. It then passes that on to DemotePrimary

It's not required, but might potentially prevent future unintended behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could move this function from replication.go to emergency_reparenter.go, as that's the only place it's used from.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that in a follow up PR, just to make sure the diffs are easier to read (and so this doesn't require another review round on this PR).

if err != nil {
msg := "replica %v thinks it's primary but we failed to demote it: %v"
err = vterrors.Wrapf(err, msg, alias, err)
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vtctl/reparentutil/replication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ type stopReplicationAndBuildStatusMapsTestTMClient struct {
stopReplicationAndGetStatusDelays map[string]time.Duration
}

func (fake *stopReplicationAndBuildStatusMapsTestTMClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
func (fake *stopReplicationAndBuildStatusMapsTestTMClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet, force bool) (*replicationdatapb.PrimaryStatus, error) {
if tablet.Alias == nil {
return nil, assert.AnError
}
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vttablet/faketmclient/fake_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ func (client *FakeTabletManagerClient) ReadReparentJournalInfo(ctx context.Conte
}

// DemotePrimary is part of the tmclient.TabletManagerClient interface.
func (client *FakeTabletManagerClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
func (client *FakeTabletManagerClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet, force bool) (*replicationdatapb.PrimaryStatus, error) {
return nil, nil
}

Expand Down
4 changes: 2 additions & 2 deletions go/vt/vttablet/grpctmclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1191,13 +1191,13 @@ func (client *Client) InitReplica(ctx context.Context, tablet *topodatapb.Tablet
}

// DemotePrimary is part of the tmclient.TabletManagerClient interface.
func (client *Client) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
func (client *Client) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet, force bool) (*replicationdatapb.PrimaryStatus, error) {
c, closer, err := client.dialer.dial(ctx, tablet)
if err != nil {
return nil, err
}
defer closer.Close()
response, err := c.DemotePrimary(ctx, &tabletmanagerdatapb.DemotePrimaryRequest{})
response, err := c.DemotePrimary(ctx, &tabletmanagerdatapb.DemotePrimaryRequest{Force: force})
if err != nil {
return nil, vterrors.FromGRPC(err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vttablet/grpctmserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func (s *server) DemotePrimary(ctx context.Context, request *tabletmanagerdatapb
defer s.tm.HandleRPCPanic(ctx, "DemotePrimary", request, response, true /*verbose*/, &err)
ctx = callinfo.GRPCCallInfo(ctx)
response = &tabletmanagerdatapb.DemotePrimaryResponse{}
status, err := s.tm.DemotePrimary(ctx)
status, err := s.tm.DemotePrimary(ctx, request.Force)
if err == nil {
response.PrimaryStatus = status
}
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vttablet/tabletmanager/rpc_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ type RPCTM interface {

InitReplica(ctx context.Context, parent *topodatapb.TabletAlias, replicationPosition string, timeCreatedNS int64, semiSync bool) error

DemotePrimary(ctx context.Context) (*replicationdatapb.PrimaryStatus, error)
DemotePrimary(ctx context.Context, force bool) (*replicationdatapb.PrimaryStatus, error)

UndoDemotePrimary(ctx context.Context, semiSync bool) error

Expand Down
39 changes: 31 additions & 8 deletions go/vt/vttablet/tabletmanager/rpc_replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,20 +548,20 @@ func (tm *TabletManager) InitReplica(ctx context.Context, parent *topodatapb.Tab
// or on a tablet that already transitioned to REPLICA.
//
// If a step fails in the middle, it will try to undo any changes it made.
func (tm *TabletManager) DemotePrimary(ctx context.Context) (*replicationdatapb.PrimaryStatus, error) {
func (tm *TabletManager) DemotePrimary(ctx context.Context, force bool) (*replicationdatapb.PrimaryStatus, error) {
log.Infof("DemotePrimary")
if err := tm.waitForGrantsToHaveApplied(ctx); err != nil {
return nil, err
}
// The public version always reverts on partial failure.
return tm.demotePrimary(ctx, true /* revertPartialFailure */)
return tm.demotePrimary(ctx, true /* revertPartialFailure */, force)
}

// demotePrimary implements DemotePrimary with an additional, private option.
//
// If revertPartialFailure is true, and a step fails in the middle, it will try
// to undo any changes it made.
func (tm *TabletManager) demotePrimary(ctx context.Context, revertPartialFailure bool) (primaryStatus *replicationdatapb.PrimaryStatus, finalErr error) {
func (tm *TabletManager) demotePrimary(ctx context.Context, revertPartialFailure bool, force bool) (primaryStatus *replicationdatapb.PrimaryStatus, finalErr error) {
if err := tm.lock(ctx); err != nil {
return nil, err
}
Expand Down Expand Up @@ -623,13 +623,37 @@ func (tm *TabletManager) demotePrimary(ctx context.Context, revertPartialFailure
}()
}

// Now we know no writes are in-flight and no new writes can occur.
// We just need to wait for no write being blocked on semi-sync ACKs.
err = tm.SemiSyncMonitor.WaitUntilSemiSyncUnblocked(ctx)
isSemiSyncBlocked, err := tm.MysqlDaemon.IsSemiSyncBlocked(ctx)
Comment thread
mattlord marked this conversation as resolved.
if err != nil {
return nil, err
}

if force && isSemiSyncBlocked {
if tm.isPrimarySideSemiSyncEnabled(ctx) {
Comment thread
timvaillancourt marked this conversation as resolved.
// Disable the primary side semi-sync to unblock the writes.
if err := tm.fixSemiSync(ctx, topodatapb.TabletType_REPLICA, SemiSyncActionSet); err != nil {
return nil, err
}
defer func() {
if finalErr != nil && revertPartialFailure && wasPrimary {
// enable primary-side semi-sync again
if err := tm.fixSemiSync(ctx, topodatapb.TabletType_PRIMARY, SemiSyncActionSet); err != nil {
log.Warningf("fixSemiSync(PRIMARY) failed during revert: %v", err)
}
}
}()
}
} else {
// TODO: Shouldn't we better just fail here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO is not clear to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattlord I think Arthur's intention with this comment is: in the else semi-sync is either not-blocked, or if it is blocked, the idea is it will never be successful because there are no ackers

I've lost the data to prove this, but I'm not sure if the "never successful" part of this is really true, because in my experience on a 100% VTOrc + semi-sync production (all unplanned reparents are ERS), we only saw the "forever demoting PRIMARY" sometimes, certainly not all ERSs 🤔. I'd like to understand in what scenarios this does/doesn't happen, to help me explain why this only happened "sometimes" in the wild at Slack

cc @rvrangel / @tanjinx to help validate this last point

@timvaillancourt timvaillancourt Nov 11, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a bit more, and I think the discrepancy in my mind is: an ERS is used for a plethora of problems in VTOrc (and when exec'd manually), some totally unrelated to the health of the replication chain, for example PrimaryTabletDeleted

To further complicate things, some problems are just intermittent, meaning a loss of ackers is temporary and they DO come back to a healthy state a few seconds later, due to overload, network partitions/issues, etc. I'm pretty sure this is something we saw in Slack's production, for example

So my fear is: if we always disable semi-sync when we are waiting on acks during demotePrimary, there may be scenarios where this is not ideal, or happens too soon (and ackers would come back). No immediate solutions. ERS is hard

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first step of ERS is to stop all replicas from replicating from the primary, so if that's successful no replica should come back.

I can imagine that we might not end up stopping replication on all replicas (for whatever reason that is), but the DemotePrimary call will happen in parallel and at least attempt to demote the primary. That might fail as well, but in that case ERS will just stop and not proceed and the cluster will be in an indeterminate state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first step of ERS is to stop all replicas from replicating from the primary, so if that's successful no replica should come back.

@arthurschreiber that's a good point, hmm

I guess I should just take a step back then, and just say: we didn't encounter the stalled-demoting PRIMARY as a norm at Slack: an environment using 100% semi-sync and VTOrc/ERS for unplanned reparents. This scenario happened, but the "usual" case was a clean demotion to REPLICA

If I forget this experience in the wild (maybe I should 😬?) the code changes here all look fine to me, but I'm worried we're missing something, and I feel ERS/reparents aren't tested extensively enough (in general - not this PR specifically) for us to be sure. Perhaps I'll dig into the changes between v19 and now to try to explain why this was the "exception" and not the "norm" at Slack 😕


// Now we know no writes are in-flight and no new writes can occur.
// We just need to wait for no write being blocked on semi-sync ACKs.
err = tm.SemiSyncMonitor.WaitUntilSemiSyncUnblocked(ctx)
if err != nil {
return nil, err
}
}

// We can now set MySQL to super_read_only mode. If we are already super_read_only because of a
// previous demotion, or because we are not primary anyway, this should be
// idempotent.
Expand All @@ -651,8 +675,7 @@ func (tm *TabletManager) demotePrimary(ctx context.Context, revertPartialFailure
}
}()

// 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.
// If it is enabled then we should turn it off and revert in case of failure.
// If we haven't disabled the primary side semi-sync so far, do it now.
if tm.isPrimarySideSemiSyncEnabled(ctx) {
// If using semi-sync, we need to disable primary-side.
if err := tm.fixSemiSync(ctx, topodatapb.TabletType_REPLICA, SemiSyncActionSet); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions go/vt/vttablet/tabletmanager/rpc_replication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestDemotePrimaryStalled(t *testing.T) {
}

go func() {
tm.demotePrimary(context.Background(), false)
tm.demotePrimary(context.Background(), false /* revertPartialFailure */, false /* force */)
}()
// We make IsServing stall by making it wait on a channel.
// This should cause the demote primary operation to be stalled.
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestDemotePrimaryWaitingForSemiSyncUnblock(t *testing.T) {
// Start the demote primary operation in a go routine.
var demotePrimaryFinished atomic.Bool
go func() {
_, err := tm.demotePrimary(ctx, false)
_, err := tm.demotePrimary(ctx, false /* revertPartialFailure */, false /* force */)
require.NoError(t, err)
demotePrimaryFinished.Store(true)
}()
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vttablet/tabletmanager/shard_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func (tm *TabletManager) endPrimaryTerm(ctx context.Context, primaryAlias *topod
log.Infof("Active reparents are enabled; converting MySQL to replica.")
demotePrimaryCtx, cancelDemotePrimary := context.WithTimeout(ctx, topo.RemoteOperationTimeout)
defer cancelDemotePrimary()
if _, err := tm.demotePrimary(demotePrimaryCtx, false /* revertPartialFailure */); err != nil {
if _, err := tm.demotePrimary(demotePrimaryCtx, false /* revertPartialFailure */, true /* force */); err != nil {
return vterrors.Wrap(err, "failed to demote primary")
}
setPrimaryCtx, cancelSetPrimary := context.WithTimeout(ctx, topo.RemoteOperationTimeout)
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vttablet/tmclient/rpc_client_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ type TabletManagerClient interface {

// DemotePrimary tells the soon-to-be-former primary it's going to change,
// and it should go read-only and return its current position.
DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error)
DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet, force bool) (*replicationdatapb.PrimaryStatus, error)

// UndoDemotePrimary reverts all changes made by DemotePrimary
// To be used if we are unable to promote the chosen new primary
Expand Down
6 changes: 3 additions & 3 deletions go/vt/vttablet/tmrpctest/test_tm_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,20 +1266,20 @@ func tmRPCTestInitReplicaPanic(ctx context.Context, t *testing.T, client tmclien
expectHandleRPCPanic(t, "InitReplica", true /*verbose*/, err)
}

func (fra *fakeRPCTM) DemotePrimary(ctx context.Context) (*replicationdatapb.PrimaryStatus, error) {
func (fra *fakeRPCTM) DemotePrimary(ctx context.Context, force bool) (*replicationdatapb.PrimaryStatus, error) {
if fra.panics {
panic(fmt.Errorf("test-triggered panic"))
}
return testPrimaryStatus, nil
}

func tmRPCTestDemotePrimary(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, tablet *topodatapb.Tablet) {
PrimaryStatus, err := client.DemotePrimary(ctx, tablet)
PrimaryStatus, err := client.DemotePrimary(ctx, tablet, false)
compareError(t, "DemotePrimary", err, PrimaryStatus.Position, testPrimaryStatus.Position)
}

func tmRPCTestDemotePrimaryPanic(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, tablet *topodatapb.Tablet) {
_, err := client.DemotePrimary(ctx, tablet)
_, err := client.DemotePrimary(ctx, tablet, false)
expectHandleRPCPanic(t, "DemotePrimary", true /*verbose*/, err)
}

Expand Down
1 change: 1 addition & 0 deletions proto/tabletmanagerdata.proto
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ message InitReplicaResponse {
}

message DemotePrimaryRequest {
bool force = 1;
}

message DemotePrimaryResponse {
Expand Down
Loading
Loading