vitess.io/vitess@v0.16.2/go/vt/vtctl/reparentutil/replication_test.go (about)

     1  /*
     2  Copyright 2021 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 reparentutil
    18  
    19  import (
    20  	"context"
    21  	"os"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  
    30  	_flag "vitess.io/vitess/go/internal/flag"
    31  	"vitess.io/vitess/go/mysql"
    32  	"vitess.io/vitess/go/vt/logutil"
    33  	"vitess.io/vitess/go/vt/topo"
    34  	"vitess.io/vitess/go/vt/topo/topoproto"
    35  	"vitess.io/vitess/go/vt/topotools/events"
    36  	"vitess.io/vitess/go/vt/vterrors"
    37  	"vitess.io/vitess/go/vt/vttablet/tmclient"
    38  
    39  	replicationdatapb "vitess.io/vitess/go/vt/proto/replicationdata"
    40  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    41  )
    42  
    43  func TestMain(m *testing.M) {
    44  	_flag.ParseFlagsForTest()
    45  	os.Exit(m.Run())
    46  }
    47  
    48  func TestFindValidEmergencyReparentCandidates(t *testing.T) {
    49  	t.Parallel()
    50  
    51  	tests := []struct {
    52  		name             string
    53  		statusMap        map[string]*replicationdatapb.StopReplicationStatus
    54  		primaryStatusMap map[string]*replicationdatapb.PrimaryStatus
    55  		// Note: for these tests, it's simpler to compare keys than actual
    56  		// mysql.Postion structs, which are just thin wrappers around the
    57  		// mysql.GTIDSet interface. If a tablet alias makes it into the map, we
    58  		// know it was chosen by the method, and that either
    59  		// mysql.DecodePosition was successful (in the primary case) or
    60  		// status.FindErrantGTIDs was successful (in the replica case). If the
    61  		// former is not true, then the function should return an error. If the
    62  		// latter is not true, then the tablet alias will not be in the map. The
    63  		// point is, the combination of (1) whether the test should error and
    64  		// (2) the set of keys we expect in the map is enough to fully assert on
    65  		// the correctness of the behavior of this functional unit.
    66  		expected  []string
    67  		shouldErr bool
    68  	}{
    69  		{
    70  			name: "success",
    71  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
    72  				"r1": {
    73  					After: &replicationdatapb.Status{
    74  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
    75  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
    76  					},
    77  				},
    78  				"r2": {
    79  					After: &replicationdatapb.Status{
    80  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
    81  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
    82  					},
    83  				},
    84  			},
    85  			primaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{
    86  				"p1": {
    87  					Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
    88  				},
    89  			},
    90  			expected:  []string{"r1", "r2", "p1"},
    91  			shouldErr: false,
    92  		}, {
    93  			name: "success for single tablet",
    94  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
    95  				"r1": {
    96  					After: &replicationdatapb.Status{
    97  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
    98  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,AAAAAAAA-71CA-11E1-9E33-C80AA9429562:1",
    99  					},
   100  				},
   101  			},
   102  			primaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   103  			expected:         []string{"r1"},
   104  			shouldErr:        false,
   105  		},
   106  		{
   107  			name: "mixed replication modes",
   108  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
   109  				"r1": {
   110  					After: &replicationdatapb.Status{
   111  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   112  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
   113  					},
   114  				},
   115  				"r2": {
   116  					After: &replicationdatapb.Status{
   117  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   118  						RelayLogPosition: "FilePos/mysql-bin.0001:10",
   119  					},
   120  				},
   121  			},
   122  			expected:  nil,
   123  			shouldErr: true,
   124  		},
   125  		{
   126  			name: "tablet without relay log position",
   127  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
   128  				"r1": {
   129  					After: &replicationdatapb.Status{
   130  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   131  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
   132  					},
   133  				},
   134  				"r2": {
   135  					After: &replicationdatapb.Status{
   136  						RelayLogPosition: "",
   137  					},
   138  				},
   139  			},
   140  			expected:  nil,
   141  			shouldErr: true,
   142  		},
   143  		{
   144  			name: "non-GTID-based",
   145  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
   146  				"r1": {
   147  					After: &replicationdatapb.Status{
   148  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   149  						RelayLogPosition: "FilePos/mysql-bin.0001:100",
   150  					},
   151  				},
   152  				"r2": {
   153  					After: &replicationdatapb.Status{
   154  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   155  						RelayLogPosition: "FilePos/mysql-bin.0001:10",
   156  					},
   157  				},
   158  			},
   159  			expected:  []string{"r1", "r2"},
   160  			shouldErr: false,
   161  		},
   162  		{
   163  			name: "tablet with errant GTIDs is excluded",
   164  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
   165  				"r1": {
   166  					After: &replicationdatapb.Status{
   167  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   168  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
   169  					},
   170  				},
   171  				"errant": {
   172  					After: &replicationdatapb.Status{
   173  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   174  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,AAAAAAAA-71CA-11E1-9E33-C80AA9429562:1",
   175  					},
   176  				},
   177  			},
   178  			primaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{
   179  				"p1": {
   180  					Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
   181  				},
   182  			},
   183  			expected:  []string{"r1", "p1"},
   184  			shouldErr: false,
   185  		},
   186  		{
   187  			name: "bad primary position fails the call",
   188  			statusMap: map[string]*replicationdatapb.StopReplicationStatus{
   189  				"r1": {
   190  					After: &replicationdatapb.Status{
   191  						SourceUuid:       "3E11FA47-71CA-11E1-9E33-C80AA9429562",
   192  						RelayLogPosition: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5",
   193  					},
   194  				},
   195  			},
   196  			primaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{
   197  				"p1": {
   198  					Position: "InvalidFlavor/1234",
   199  				},
   200  			},
   201  			expected:  nil,
   202  			shouldErr: true,
   203  		},
   204  	}
   205  
   206  	for _, tt := range tests {
   207  		tt := tt
   208  
   209  		t.Run(tt.name, func(t *testing.T) {
   210  			t.Parallel()
   211  
   212  			actual, err := FindValidEmergencyReparentCandidates(tt.statusMap, tt.primaryStatusMap)
   213  			if tt.shouldErr {
   214  				assert.Error(t, err)
   215  				return
   216  			}
   217  
   218  			assert.NoError(t, err)
   219  
   220  			keys := make([]string, 0, len(actual))
   221  			for key := range actual {
   222  				keys = append(keys, key)
   223  			}
   224  			assert.ElementsMatch(t, tt.expected, keys)
   225  		})
   226  	}
   227  }
   228  
   229  // stopReplicationAndBuildStatusMapsTestTMClient implements
   230  // tmclient.TabletManagerClient to facilitate testing of
   231  // stopReplicationAndBuildStatusMaps.
   232  type stopReplicationAndBuildStatusMapsTestTMClient struct {
   233  	tmclient.TabletManagerClient
   234  
   235  	demotePrimaryResults map[string]*struct {
   236  		PrimaryStatus *replicationdatapb.PrimaryStatus
   237  		Err           error
   238  	}
   239  	demotePrimaryDelays map[string]time.Duration
   240  
   241  	stopReplicationAndGetStatusResults map[string]*struct {
   242  		StopStatus *replicationdatapb.StopReplicationStatus
   243  		Err        error
   244  	}
   245  	stopReplicationAndGetStatusDelays map[string]time.Duration
   246  }
   247  
   248  func (fake *stopReplicationAndBuildStatusMapsTestTMClient) DemotePrimary(ctx context.Context, tablet *topodatapb.Tablet) (*replicationdatapb.PrimaryStatus, error) {
   249  	if tablet.Alias == nil {
   250  		return nil, assert.AnError
   251  	}
   252  
   253  	key := topoproto.TabletAliasString(tablet.Alias)
   254  
   255  	if delay, ok := fake.demotePrimaryDelays[key]; ok {
   256  		select {
   257  		case <-time.After(delay):
   258  		case <-ctx.Done():
   259  			return nil, ctx.Err()
   260  		}
   261  	}
   262  
   263  	if result, ok := fake.demotePrimaryResults[key]; ok {
   264  		return result.PrimaryStatus, result.Err
   265  	}
   266  
   267  	return nil, assert.AnError
   268  }
   269  
   270  func (fake *stopReplicationAndBuildStatusMapsTestTMClient) StopReplicationAndGetStatus(ctx context.Context, tablet *topodatapb.Tablet, mode replicationdatapb.StopReplicationMode) (*replicationdatapb.StopReplicationStatus, error) {
   271  	if tablet.Alias == nil {
   272  		return nil, assert.AnError
   273  	}
   274  
   275  	key := topoproto.TabletAliasString(tablet.Alias)
   276  
   277  	if delay, ok := fake.stopReplicationAndGetStatusDelays[key]; ok {
   278  		select {
   279  		case <-time.After(delay):
   280  		case <-ctx.Done():
   281  			return nil, ctx.Err()
   282  		}
   283  	}
   284  
   285  	if result, ok := fake.stopReplicationAndGetStatusResults[key]; ok {
   286  		return result.StopStatus, result.Err
   287  	}
   288  
   289  	return nil, assert.AnError
   290  }
   291  
   292  func Test_stopReplicationAndBuildStatusMaps(t *testing.T) {
   293  	ctx := context.Background()
   294  	logger := logutil.NewMemoryLogger()
   295  	tests := []struct {
   296  		name                     string
   297  		durability               string
   298  		tmc                      *stopReplicationAndBuildStatusMapsTestTMClient
   299  		tabletMap                map[string]*topo.TabletInfo
   300  		stopReplicasTimeout      time.Duration
   301  		ignoredTablets           sets.Set[string]
   302  		tabletToWaitFor          *topodatapb.TabletAlias
   303  		expectedStatusMap        map[string]*replicationdatapb.StopReplicationStatus
   304  		expectedPrimaryStatusMap map[string]*replicationdatapb.PrimaryStatus
   305  		expectedTabletsReachable []*topodatapb.Tablet
   306  		shouldErr                bool
   307  	}{
   308  		{
   309  			name:       "success",
   310  			durability: "none",
   311  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   312  				stopReplicationAndGetStatusResults: map[string]*struct {
   313  					StopStatus *replicationdatapb.StopReplicationStatus
   314  					Err        error
   315  				}{
   316  					"zone1-0000000100": {
   317  						StopStatus: &replicationdatapb.StopReplicationStatus{
   318  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   319  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   320  						},
   321  					},
   322  					"zone1-0000000101": {
   323  						StopStatus: &replicationdatapb.StopReplicationStatus{
   324  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   325  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   326  						},
   327  					},
   328  				},
   329  			},
   330  			tabletMap: map[string]*topo.TabletInfo{
   331  				"zone1-0000000100": {
   332  					Tablet: &topodatapb.Tablet{
   333  						Type: topodatapb.TabletType_REPLICA,
   334  						Alias: &topodatapb.TabletAlias{
   335  							Cell: "zone1",
   336  							Uid:  100,
   337  						},
   338  					},
   339  				},
   340  				"zone1-0000000101": {
   341  					Tablet: &topodatapb.Tablet{
   342  						Type: topodatapb.TabletType_REPLICA,
   343  						Alias: &topodatapb.TabletAlias{
   344  							Cell: "zone1",
   345  							Uid:  101,
   346  						},
   347  					},
   348  				},
   349  			},
   350  			ignoredTablets: sets.New[string](),
   351  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   352  				"zone1-0000000100": {
   353  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   354  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   355  				},
   356  				"zone1-0000000101": {
   357  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   358  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   359  				},
   360  			},
   361  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   362  			expectedTabletsReachable: []*topodatapb.Tablet{{
   363  				Type: topodatapb.TabletType_REPLICA,
   364  				Alias: &topodatapb.TabletAlias{
   365  					Cell: "zone1",
   366  					Uid:  100,
   367  				},
   368  			}, {
   369  				Type: topodatapb.TabletType_REPLICA,
   370  				Alias: &topodatapb.TabletAlias{
   371  					Cell: "zone1",
   372  					Uid:  101,
   373  				},
   374  			}},
   375  			shouldErr: false,
   376  		},
   377  		{
   378  			name:       "success - 2 rdonly failures",
   379  			durability: "none",
   380  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   381  				stopReplicationAndGetStatusResults: map[string]*struct {
   382  					StopStatus *replicationdatapb.StopReplicationStatus
   383  					Err        error
   384  				}{
   385  					"zone1-0000000100": {
   386  						StopStatus: &replicationdatapb.StopReplicationStatus{
   387  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   388  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   389  						},
   390  					},
   391  					"zone1-0000000101": {
   392  						StopStatus: &replicationdatapb.StopReplicationStatus{
   393  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   394  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   395  						},
   396  					},
   397  					"zone1-0000000102": {
   398  						Err: assert.AnError,
   399  					},
   400  					"zone1-0000000103": {
   401  						Err: assert.AnError,
   402  					},
   403  				},
   404  			},
   405  			tabletMap: map[string]*topo.TabletInfo{
   406  				"zone1-0000000100": {
   407  					Tablet: &topodatapb.Tablet{
   408  						Type: topodatapb.TabletType_REPLICA,
   409  						Alias: &topodatapb.TabletAlias{
   410  							Cell: "zone1",
   411  							Uid:  100,
   412  						},
   413  					},
   414  				},
   415  				"zone1-0000000101": {
   416  					Tablet: &topodatapb.Tablet{
   417  						Type: topodatapb.TabletType_REPLICA,
   418  						Alias: &topodatapb.TabletAlias{
   419  							Cell: "zone1",
   420  							Uid:  101,
   421  						},
   422  					},
   423  				},
   424  				"zone1-0000000102": {
   425  					Tablet: &topodatapb.Tablet{
   426  						Type: topodatapb.TabletType_RDONLY,
   427  						Alias: &topodatapb.TabletAlias{
   428  							Cell: "zone1",
   429  							Uid:  102,
   430  						},
   431  					},
   432  				},
   433  				"zone1-0000000103": {
   434  					Tablet: &topodatapb.Tablet{
   435  						Type: topodatapb.TabletType_RDONLY,
   436  						Alias: &topodatapb.TabletAlias{
   437  							Cell: "zone1",
   438  							Uid:  103,
   439  						},
   440  					},
   441  				},
   442  			},
   443  			ignoredTablets: sets.New[string](),
   444  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   445  				"zone1-0000000100": {
   446  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   447  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   448  				},
   449  				"zone1-0000000101": {
   450  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   451  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   452  				},
   453  			},
   454  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   455  			expectedTabletsReachable: []*topodatapb.Tablet{{
   456  				Type: topodatapb.TabletType_REPLICA,
   457  				Alias: &topodatapb.TabletAlias{
   458  					Cell: "zone1",
   459  					Uid:  100,
   460  				},
   461  			}, {
   462  				Type: topodatapb.TabletType_REPLICA,
   463  				Alias: &topodatapb.TabletAlias{
   464  					Cell: "zone1",
   465  					Uid:  101,
   466  				},
   467  			}},
   468  			shouldErr: false,
   469  		},
   470  		{
   471  			name:       "success - 1 rdonly and 1 replica failures",
   472  			durability: "semi_sync",
   473  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   474  				stopReplicationAndGetStatusResults: map[string]*struct {
   475  					StopStatus *replicationdatapb.StopReplicationStatus
   476  					Err        error
   477  				}{
   478  					"zone1-0000000100": {
   479  						StopStatus: &replicationdatapb.StopReplicationStatus{
   480  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   481  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   482  						},
   483  					},
   484  					"zone1-0000000101": {
   485  						StopStatus: &replicationdatapb.StopReplicationStatus{
   486  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   487  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   488  						},
   489  					},
   490  					"zone1-0000000102": {
   491  						Err: assert.AnError,
   492  					},
   493  					"zone1-0000000103": {
   494  						Err: assert.AnError,
   495  					},
   496  				},
   497  			},
   498  			tabletMap: map[string]*topo.TabletInfo{
   499  				"zone1-0000000100": {
   500  					Tablet: &topodatapb.Tablet{
   501  						Type: topodatapb.TabletType_REPLICA,
   502  						Alias: &topodatapb.TabletAlias{
   503  							Cell: "zone1",
   504  							Uid:  100,
   505  						},
   506  					},
   507  				},
   508  				"zone1-0000000101": {
   509  					Tablet: &topodatapb.Tablet{
   510  						Type: topodatapb.TabletType_REPLICA,
   511  						Alias: &topodatapb.TabletAlias{
   512  							Cell: "zone1",
   513  							Uid:  101,
   514  						},
   515  					},
   516  				},
   517  				"zone1-0000000102": {
   518  					Tablet: &topodatapb.Tablet{
   519  						Type: topodatapb.TabletType_REPLICA,
   520  						Alias: &topodatapb.TabletAlias{
   521  							Cell: "zone1",
   522  							Uid:  102,
   523  						},
   524  					},
   525  				},
   526  				"zone1-0000000103": {
   527  					Tablet: &topodatapb.Tablet{
   528  						Type: topodatapb.TabletType_RDONLY,
   529  						Alias: &topodatapb.TabletAlias{
   530  							Cell: "zone1",
   531  							Uid:  103,
   532  						},
   533  					},
   534  				},
   535  			},
   536  			ignoredTablets: sets.New[string](),
   537  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   538  				"zone1-0000000100": {
   539  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   540  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   541  				},
   542  				"zone1-0000000101": {
   543  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   544  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   545  				},
   546  			},
   547  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   548  			expectedTabletsReachable: []*topodatapb.Tablet{{
   549  				Type: topodatapb.TabletType_REPLICA,
   550  				Alias: &topodatapb.TabletAlias{
   551  					Cell: "zone1",
   552  					Uid:  100,
   553  				},
   554  			}, {
   555  				Type: topodatapb.TabletType_REPLICA,
   556  				Alias: &topodatapb.TabletAlias{
   557  					Cell: "zone1",
   558  					Uid:  101,
   559  				},
   560  			}},
   561  			shouldErr: false,
   562  		},
   563  		{
   564  			name:       "ignore tablets",
   565  			durability: "none",
   566  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   567  				stopReplicationAndGetStatusResults: map[string]*struct {
   568  					StopStatus *replicationdatapb.StopReplicationStatus
   569  					Err        error
   570  				}{
   571  					"zone1-0000000100": {
   572  						StopStatus: &replicationdatapb.StopReplicationStatus{
   573  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   574  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   575  						},
   576  					},
   577  					"zone1-0000000101": {
   578  						StopStatus: &replicationdatapb.StopReplicationStatus{
   579  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   580  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   581  						},
   582  					},
   583  				},
   584  			},
   585  			tabletMap: map[string]*topo.TabletInfo{
   586  				"zone1-0000000100": {
   587  					Tablet: &topodatapb.Tablet{
   588  						Type: topodatapb.TabletType_REPLICA,
   589  						Alias: &topodatapb.TabletAlias{
   590  							Cell: "zone1",
   591  							Uid:  100,
   592  						},
   593  					},
   594  				},
   595  				"zone1-0000000101": {
   596  					Tablet: &topodatapb.Tablet{
   597  						Type: topodatapb.TabletType_REPLICA,
   598  						Alias: &topodatapb.TabletAlias{
   599  							Cell: "zone1",
   600  							Uid:  101,
   601  						},
   602  					},
   603  				},
   604  			},
   605  			ignoredTablets: sets.New[string]("zone1-0000000100"),
   606  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   607  				"zone1-0000000101": {
   608  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   609  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   610  				},
   611  			},
   612  			expectedTabletsReachable: []*topodatapb.Tablet{{
   613  				Type: topodatapb.TabletType_REPLICA,
   614  				Alias: &topodatapb.TabletAlias{
   615  					Cell: "zone1",
   616  					Uid:  101,
   617  				},
   618  			}},
   619  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   620  			shouldErr:                false,
   621  		},
   622  		{
   623  			name:       "have PRIMARY tablet and can demote",
   624  			durability: "none",
   625  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   626  				demotePrimaryResults: map[string]*struct {
   627  					PrimaryStatus *replicationdatapb.PrimaryStatus
   628  					Err           error
   629  				}{
   630  					"zone1-0000000100": {
   631  						PrimaryStatus: &replicationdatapb.PrimaryStatus{
   632  							Position: "primary-position-100",
   633  						},
   634  					},
   635  				},
   636  				stopReplicationAndGetStatusResults: map[string]*struct {
   637  					StopStatus *replicationdatapb.StopReplicationStatus
   638  					Err        error
   639  				}{
   640  					"zone1-0000000100": {
   641  						// In the tabletManager implementation of StopReplicationAndGetStatus
   642  						// we wrap the error and then send it via GRPC. This should still work as expected.
   643  						Err: vterrors.ToGRPC(vterrors.Wrap(mysql.ErrNotReplica, "before status failed")),
   644  					},
   645  					"zone1-0000000101": {
   646  						StopStatus: &replicationdatapb.StopReplicationStatus{
   647  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   648  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   649  						},
   650  					},
   651  				},
   652  			},
   653  			tabletMap: map[string]*topo.TabletInfo{
   654  				"zone1-0000000100": {
   655  					Tablet: &topodatapb.Tablet{
   656  						Type: topodatapb.TabletType_PRIMARY,
   657  						Alias: &topodatapb.TabletAlias{
   658  							Cell: "zone1",
   659  							Uid:  100,
   660  						},
   661  					},
   662  				},
   663  				"zone1-0000000101": {
   664  					Tablet: &topodatapb.Tablet{
   665  						Type: topodatapb.TabletType_REPLICA,
   666  						Alias: &topodatapb.TabletAlias{
   667  							Cell: "zone1",
   668  							Uid:  101,
   669  						},
   670  					},
   671  				},
   672  			},
   673  			ignoredTablets: sets.New[string](),
   674  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   675  				"zone1-0000000101": {
   676  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   677  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   678  				},
   679  			},
   680  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{
   681  				"zone1-0000000100": {
   682  					Position: "primary-position-100",
   683  				},
   684  			},
   685  			expectedTabletsReachable: []*topodatapb.Tablet{{
   686  				Type: topodatapb.TabletType_REPLICA,
   687  				Alias: &topodatapb.TabletAlias{
   688  					Cell: "zone1",
   689  					Uid:  100,
   690  				},
   691  			}, {
   692  				Type: topodatapb.TabletType_REPLICA,
   693  				Alias: &topodatapb.TabletAlias{
   694  					Cell: "zone1",
   695  					Uid:  101,
   696  				},
   697  			}},
   698  			shouldErr: false,
   699  		},
   700  		{
   701  			name:       "one tablet is PRIMARY and cannot demote",
   702  			durability: "none",
   703  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   704  				demotePrimaryResults: map[string]*struct {
   705  					PrimaryStatus *replicationdatapb.PrimaryStatus
   706  					Err           error
   707  				}{
   708  					"zone1-0000000100": {
   709  						Err: assert.AnError,
   710  					},
   711  				},
   712  				stopReplicationAndGetStatusResults: map[string]*struct {
   713  					StopStatus *replicationdatapb.StopReplicationStatus
   714  					Err        error
   715  				}{
   716  					"zone1-0000000100": {
   717  						Err: mysql.ErrNotReplica,
   718  					},
   719  					"zone1-0000000101": {
   720  						StopStatus: &replicationdatapb.StopReplicationStatus{
   721  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   722  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   723  						},
   724  					},
   725  				},
   726  			},
   727  			tabletMap: map[string]*topo.TabletInfo{
   728  				"zone1-0000000100": {
   729  					Tablet: &topodatapb.Tablet{
   730  						Type: topodatapb.TabletType_PRIMARY,
   731  						Alias: &topodatapb.TabletAlias{
   732  							Cell: "zone1",
   733  							Uid:  100,
   734  						},
   735  					},
   736  				},
   737  				"zone1-0000000101": {
   738  					Tablet: &topodatapb.Tablet{
   739  						Type: topodatapb.TabletType_REPLICA,
   740  						Alias: &topodatapb.TabletAlias{
   741  							Cell: "zone1",
   742  							Uid:  101,
   743  						},
   744  					},
   745  				},
   746  			},
   747  			ignoredTablets: sets.New[string](),
   748  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   749  				"zone1-0000000101": {
   750  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   751  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   752  				},
   753  			},
   754  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{}, // zone1-0000000100 fails to demote, so does not appear
   755  			expectedTabletsReachable: []*topodatapb.Tablet{{
   756  				Type: topodatapb.TabletType_REPLICA,
   757  				Alias: &topodatapb.TabletAlias{
   758  					Cell: "zone1",
   759  					Uid:  101,
   760  				},
   761  			}},
   762  			shouldErr: false,
   763  		},
   764  		{
   765  			name:       "multiple tablets are PRIMARY and cannot demote",
   766  			durability: "none",
   767  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   768  				demotePrimaryResults: map[string]*struct {
   769  					PrimaryStatus *replicationdatapb.PrimaryStatus
   770  					Err           error
   771  				}{
   772  					"zone1-0000000100": {
   773  						Err: assert.AnError,
   774  					},
   775  					"zone1-0000000101": {
   776  						Err: assert.AnError,
   777  					},
   778  				},
   779  				stopReplicationAndGetStatusResults: map[string]*struct {
   780  					StopStatus *replicationdatapb.StopReplicationStatus
   781  					Err        error
   782  				}{
   783  					"zone1-0000000100": {
   784  						Err: mysql.ErrNotReplica,
   785  					},
   786  					"zone1-0000000101": {
   787  						Err: mysql.ErrNotReplica,
   788  					},
   789  				},
   790  			},
   791  			tabletMap: map[string]*topo.TabletInfo{
   792  				"zone1-0000000100": {
   793  					Tablet: &topodatapb.Tablet{
   794  						Type: topodatapb.TabletType_PRIMARY,
   795  						Alias: &topodatapb.TabletAlias{
   796  							Cell: "zone1",
   797  							Uid:  100,
   798  						},
   799  					},
   800  				},
   801  				"zone1-0000000101": {
   802  					Tablet: &topodatapb.Tablet{
   803  						Type: topodatapb.TabletType_PRIMARY,
   804  						Alias: &topodatapb.TabletAlias{
   805  							Cell: "zone1",
   806  							Uid:  101,
   807  						},
   808  					},
   809  				},
   810  			},
   811  			ignoredTablets:           sets.New[string](),
   812  			expectedStatusMap:        nil,
   813  			expectedPrimaryStatusMap: nil,
   814  			expectedTabletsReachable: nil,
   815  			shouldErr:                true, // we get multiple errors, so we fail
   816  		},
   817  		{
   818  			name:       "stopReplicasTimeout exceeded",
   819  			durability: "none",
   820  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   821  				stopReplicationAndGetStatusDelays: map[string]time.Duration{
   822  					"zone1-0000000100": time.Minute, // zone1-0000000100 will timeout and not be included
   823  				},
   824  				stopReplicationAndGetStatusResults: map[string]*struct {
   825  					StopStatus *replicationdatapb.StopReplicationStatus
   826  					Err        error
   827  				}{
   828  					"zone1-0000000100": {
   829  						StopStatus: &replicationdatapb.StopReplicationStatus{
   830  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   831  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
   832  						},
   833  					},
   834  					"zone1-0000000101": {
   835  						StopStatus: &replicationdatapb.StopReplicationStatus{
   836  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   837  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   838  						},
   839  					},
   840  				},
   841  			},
   842  			tabletMap: map[string]*topo.TabletInfo{
   843  				"zone1-0000000100": {
   844  					Tablet: &topodatapb.Tablet{
   845  						Type: topodatapb.TabletType_REPLICA,
   846  						Alias: &topodatapb.TabletAlias{
   847  							Cell: "zone1",
   848  							Uid:  100,
   849  						},
   850  					},
   851  				},
   852  				"zone1-0000000101": {
   853  					Tablet: &topodatapb.Tablet{
   854  						Type: topodatapb.TabletType_REPLICA,
   855  						Alias: &topodatapb.TabletAlias{
   856  							Cell: "zone1",
   857  							Uid:  101,
   858  						},
   859  					},
   860  				},
   861  			},
   862  			stopReplicasTimeout: time.Millisecond * 5,
   863  			ignoredTablets:      sets.New[string](),
   864  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   865  				"zone1-0000000101": {
   866  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   867  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   868  				},
   869  			},
   870  			expectedTabletsReachable: []*topodatapb.Tablet{{
   871  				Type: topodatapb.TabletType_REPLICA,
   872  				Alias: &topodatapb.TabletAlias{
   873  					Cell: "zone1",
   874  					Uid:  101,
   875  				},
   876  			}},
   877  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   878  			shouldErr:                false,
   879  		},
   880  		{
   881  			name:       "one tablet fails to StopReplication",
   882  			durability: "none",
   883  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   884  				stopReplicationAndGetStatusResults: map[string]*struct {
   885  					StopStatus *replicationdatapb.StopReplicationStatus
   886  					Err        error
   887  				}{
   888  					"zone1-0000000100": {
   889  						Err: assert.AnError, // not being mysql.ErrNotReplica will not cause us to call DemotePrimary
   890  					},
   891  					"zone1-0000000101": {
   892  						StopStatus: &replicationdatapb.StopReplicationStatus{
   893  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   894  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   895  						},
   896  					},
   897  				},
   898  			},
   899  			tabletMap: map[string]*topo.TabletInfo{
   900  				"zone1-0000000100": {
   901  					Tablet: &topodatapb.Tablet{
   902  						Type: topodatapb.TabletType_REPLICA,
   903  						Alias: &topodatapb.TabletAlias{
   904  							Cell: "zone1",
   905  							Uid:  100,
   906  						},
   907  					},
   908  				},
   909  				"zone1-0000000101": {
   910  					Tablet: &topodatapb.Tablet{
   911  						Type: topodatapb.TabletType_REPLICA,
   912  						Alias: &topodatapb.TabletAlias{
   913  							Cell: "zone1",
   914  							Uid:  101,
   915  						},
   916  					},
   917  				},
   918  			},
   919  			ignoredTablets: sets.New[string](),
   920  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
   921  				"zone1-0000000101": {
   922  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
   923  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   924  				},
   925  			},
   926  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
   927  			expectedTabletsReachable: []*topodatapb.Tablet{{
   928  				Type: topodatapb.TabletType_REPLICA,
   929  				Alias: &topodatapb.TabletAlias{
   930  					Cell: "zone1",
   931  					Uid:  101,
   932  				},
   933  			}},
   934  			shouldErr: false,
   935  		},
   936  		{
   937  			name:       "multiple tablets fail StopReplication",
   938  			durability: "none",
   939  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   940  				stopReplicationAndGetStatusResults: map[string]*struct {
   941  					StopStatus *replicationdatapb.StopReplicationStatus
   942  					Err        error
   943  				}{
   944  					"zone1-0000000100": {
   945  						Err: assert.AnError,
   946  					},
   947  					"zone1-0000000101": {
   948  						Err: assert.AnError,
   949  					},
   950  				},
   951  			},
   952  			tabletMap: map[string]*topo.TabletInfo{
   953  				"zone1-0000000100": {
   954  					Tablet: &topodatapb.Tablet{
   955  						Type: topodatapb.TabletType_REPLICA,
   956  						Alias: &topodatapb.TabletAlias{
   957  							Cell: "zone1",
   958  							Uid:  100,
   959  						},
   960  					},
   961  				},
   962  				"zone1-0000000101": {
   963  					Tablet: &topodatapb.Tablet{
   964  						Type: topodatapb.TabletType_REPLICA,
   965  						Alias: &topodatapb.TabletAlias{
   966  							Cell: "zone1",
   967  							Uid:  101,
   968  						},
   969  					},
   970  				},
   971  			},
   972  			ignoredTablets:           sets.New[string](),
   973  			expectedStatusMap:        nil,
   974  			expectedPrimaryStatusMap: nil,
   975  			expectedTabletsReachable: nil,
   976  			shouldErr:                true,
   977  		}, {
   978  			name:       "1 tablets fail StopReplication and 1 has replication stopped",
   979  			durability: "none",
   980  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
   981  				stopReplicationAndGetStatusResults: map[string]*struct {
   982  					StopStatus *replicationdatapb.StopReplicationStatus
   983  					Err        error
   984  				}{
   985  					"zone1-0000000100": {
   986  						Err: assert.AnError,
   987  					},
   988  					"zone1-0000000101": {
   989  						StopStatus: &replicationdatapb.StopReplicationStatus{
   990  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5"},
   991  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
   992  						},
   993  					},
   994  				},
   995  			},
   996  			tabletMap: map[string]*topo.TabletInfo{
   997  				"zone1-0000000100": {
   998  					Tablet: &topodatapb.Tablet{
   999  						Type: topodatapb.TabletType_REPLICA,
  1000  						Alias: &topodatapb.TabletAlias{
  1001  							Cell: "zone1",
  1002  							Uid:  100,
  1003  						},
  1004  					},
  1005  				},
  1006  				"zone1-0000000101": {
  1007  					Tablet: &topodatapb.Tablet{
  1008  						Type: topodatapb.TabletType_REPLICA,
  1009  						Alias: &topodatapb.TabletAlias{
  1010  							Cell: "zone1",
  1011  							Uid:  101,
  1012  						},
  1013  					},
  1014  				},
  1015  			},
  1016  			ignoredTablets:           sets.New[string](),
  1017  			expectedStatusMap:        nil,
  1018  			expectedPrimaryStatusMap: nil,
  1019  			expectedTabletsReachable: nil,
  1020  			shouldErr:                true,
  1021  		},
  1022  		{
  1023  			name:       "slow tablet is the new primary requested",
  1024  			durability: "none",
  1025  			tmc: &stopReplicationAndBuildStatusMapsTestTMClient{
  1026  				stopReplicationAndGetStatusDelays: map[string]time.Duration{
  1027  					"zone1-0000000102": 1 * time.Second, // zone1-0000000102 is slow to respond but has to be included since it is the requested primary
  1028  				},
  1029  				stopReplicationAndGetStatusResults: map[string]*struct {
  1030  					StopStatus *replicationdatapb.StopReplicationStatus
  1031  					Err        error
  1032  				}{
  1033  					"zone1-0000000100": {
  1034  						StopStatus: &replicationdatapb.StopReplicationStatus{
  1035  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1036  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
  1037  						},
  1038  					},
  1039  					"zone1-0000000101": {
  1040  						StopStatus: &replicationdatapb.StopReplicationStatus{
  1041  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1042  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
  1043  						},
  1044  					},
  1045  					"zone1-0000000102": {
  1046  						StopStatus: &replicationdatapb.StopReplicationStatus{
  1047  							Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429102:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1048  							After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429102:1-9"},
  1049  						},
  1050  					},
  1051  				},
  1052  			},
  1053  			tabletMap: map[string]*topo.TabletInfo{
  1054  				"zone1-0000000100": {
  1055  					Tablet: &topodatapb.Tablet{
  1056  						Type: topodatapb.TabletType_REPLICA,
  1057  						Alias: &topodatapb.TabletAlias{
  1058  							Cell: "zone1",
  1059  							Uid:  100,
  1060  						},
  1061  					},
  1062  				},
  1063  				"zone1-0000000101": {
  1064  					Tablet: &topodatapb.Tablet{
  1065  						Type: topodatapb.TabletType_REPLICA,
  1066  						Alias: &topodatapb.TabletAlias{
  1067  							Cell: "zone1",
  1068  							Uid:  101,
  1069  						},
  1070  					},
  1071  				},
  1072  				"zone1-0000000102": {
  1073  					Tablet: &topodatapb.Tablet{
  1074  						Type: topodatapb.TabletType_REPLICA,
  1075  						Alias: &topodatapb.TabletAlias{
  1076  							Cell: "zone1",
  1077  							Uid:  102,
  1078  						},
  1079  					},
  1080  				},
  1081  			},
  1082  			tabletToWaitFor: &topodatapb.TabletAlias{
  1083  				Cell: "zone1",
  1084  				Uid:  102,
  1085  			},
  1086  			ignoredTablets: sets.New[string](),
  1087  			expectedStatusMap: map[string]*replicationdatapb.StopReplicationStatus{
  1088  				"zone1-0000000100": {
  1089  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1090  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429100:1-9"},
  1091  				},
  1092  				"zone1-0000000101": {
  1093  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1094  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429101:1-9"},
  1095  				},
  1096  				"zone1-0000000102": {
  1097  					Before: &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429102:1-5", IoState: int32(mysql.ReplicationStateRunning), SqlState: int32(mysql.ReplicationStateRunning)},
  1098  					After:  &replicationdatapb.Status{Position: "MySQL56/3E11FA47-71CA-11E1-9E33-C80AA9429102:1-9"},
  1099  				},
  1100  			},
  1101  			expectedTabletsReachable: []*topodatapb.Tablet{{
  1102  				Type: topodatapb.TabletType_REPLICA,
  1103  				Alias: &topodatapb.TabletAlias{
  1104  					Cell: "zone1",
  1105  					Uid:  100,
  1106  				},
  1107  			}, {
  1108  				Type: topodatapb.TabletType_REPLICA,
  1109  				Alias: &topodatapb.TabletAlias{
  1110  					Cell: "zone1",
  1111  					Uid:  101,
  1112  				},
  1113  			}, {
  1114  				Type: topodatapb.TabletType_REPLICA,
  1115  				Alias: &topodatapb.TabletAlias{
  1116  					Cell: "zone1",
  1117  					Uid:  102,
  1118  				},
  1119  			}},
  1120  			stopReplicasTimeout:      time.Minute,
  1121  			expectedPrimaryStatusMap: map[string]*replicationdatapb.PrimaryStatus{},
  1122  			shouldErr:                false,
  1123  		},
  1124  	}
  1125  
  1126  	for _, tt := range tests {
  1127  		tt := tt
  1128  
  1129  		t.Run(tt.name, func(t *testing.T) {
  1130  			durability, err := GetDurabilityPolicy(tt.durability)
  1131  			require.NoError(t, err)
  1132  			res, err := stopReplicationAndBuildStatusMaps(ctx, tt.tmc, &events.Reparent{}, tt.tabletMap, tt.stopReplicasTimeout, tt.ignoredTablets, tt.tabletToWaitFor, durability, logger)
  1133  			if tt.shouldErr {
  1134  				assert.Error(t, err)
  1135  				return
  1136  			}
  1137  
  1138  			assert.NoError(t, err)
  1139  			assert.Equal(t, tt.expectedStatusMap, res.statusMap, "StopReplicationStatus mismatch")
  1140  			assert.Equal(t, tt.expectedPrimaryStatusMap, res.primaryStatusMap, "PrimaryStatusMap mismatch")
  1141  			require.Equal(t, len(tt.expectedTabletsReachable), len(res.reachableTablets), "TabletsReached length mismatch")
  1142  			for idx, tablet := range res.reachableTablets {
  1143  				assert.True(t, topoproto.IsTabletInList(tablet, tt.expectedTabletsReachable), "TabletsReached[%d] not found - %s", idx, topoproto.TabletAliasString(tablet.Alias))
  1144  			}
  1145  		})
  1146  	}
  1147  }
  1148  
  1149  func TestReplicaWasRunning(t *testing.T) {
  1150  	t.Parallel()
  1151  
  1152  	tests := []struct {
  1153  		name      string
  1154  		in        *replicationdatapb.StopReplicationStatus
  1155  		expected  bool
  1156  		shouldErr bool
  1157  	}{
  1158  		{
  1159  			name: "io thread running",
  1160  			in: &replicationdatapb.StopReplicationStatus{
  1161  				Before: &replicationdatapb.Status{
  1162  					IoState:  int32(mysql.ReplicationStateRunning),
  1163  					SqlState: int32(mysql.ReplicationStateStopped),
  1164  				},
  1165  			},
  1166  			expected:  true,
  1167  			shouldErr: false,
  1168  		},
  1169  		{
  1170  			name: "sql thread running",
  1171  			in: &replicationdatapb.StopReplicationStatus{
  1172  				Before: &replicationdatapb.Status{
  1173  					IoState:  int32(mysql.ReplicationStateStopped),
  1174  					SqlState: int32(mysql.ReplicationStateRunning),
  1175  				},
  1176  			},
  1177  			expected:  true,
  1178  			shouldErr: false,
  1179  		},
  1180  		{
  1181  			name: "io and sql threads running",
  1182  			in: &replicationdatapb.StopReplicationStatus{
  1183  				Before: &replicationdatapb.Status{
  1184  					IoState:  int32(mysql.ReplicationStateRunning),
  1185  					SqlState: int32(mysql.ReplicationStateRunning),
  1186  				},
  1187  			},
  1188  			expected:  true,
  1189  			shouldErr: false,
  1190  		},
  1191  		{
  1192  			name: "no replication threads running",
  1193  			in: &replicationdatapb.StopReplicationStatus{
  1194  				Before: &replicationdatapb.Status{
  1195  					IoState:  int32(mysql.ReplicationStateStopped),
  1196  					SqlState: int32(mysql.ReplicationStateStopped),
  1197  				},
  1198  			},
  1199  			expected:  false,
  1200  			shouldErr: false,
  1201  		},
  1202  		{
  1203  			name:      "passing nil pointer results in an error",
  1204  			in:        nil,
  1205  			expected:  false,
  1206  			shouldErr: true,
  1207  		},
  1208  		{
  1209  			name: "status.Before is nil results in an error",
  1210  			in: &replicationdatapb.StopReplicationStatus{
  1211  				Before: nil,
  1212  			},
  1213  			expected:  false,
  1214  			shouldErr: true,
  1215  		},
  1216  	}
  1217  
  1218  	for _, tt := range tests {
  1219  		tt := tt
  1220  
  1221  		t.Run(tt.name, func(t *testing.T) {
  1222  			t.Parallel()
  1223  
  1224  			actual, err := ReplicaWasRunning(tt.in)
  1225  			if tt.shouldErr {
  1226  				assert.Error(t, err)
  1227  
  1228  				return
  1229  			}
  1230  
  1231  			assert.NoError(t, err)
  1232  			assert.Equal(t, tt.expected, actual)
  1233  		})
  1234  	}
  1235  }
  1236  
  1237  func TestSQLThreadWasRunning(t *testing.T) {
  1238  	t.Parallel()
  1239  
  1240  	tests := []struct {
  1241  		name      string
  1242  		in        *replicationdatapb.StopReplicationStatus
  1243  		expected  bool
  1244  		shouldErr bool
  1245  	}{
  1246  		{
  1247  			name: "io thread running",
  1248  			in: &replicationdatapb.StopReplicationStatus{
  1249  				Before: &replicationdatapb.Status{
  1250  					IoState:  int32(mysql.ReplicationStateRunning),
  1251  					SqlState: int32(mysql.ReplicationStateStopped),
  1252  				},
  1253  			},
  1254  			expected:  false,
  1255  			shouldErr: false,
  1256  		},
  1257  		{
  1258  			name: "sql thread running",
  1259  			in: &replicationdatapb.StopReplicationStatus{
  1260  				Before: &replicationdatapb.Status{
  1261  					IoState:  int32(mysql.ReplicationStateStopped),
  1262  					SqlState: int32(mysql.ReplicationStateRunning),
  1263  				},
  1264  			},
  1265  			expected:  true,
  1266  			shouldErr: false,
  1267  		},
  1268  		{
  1269  			name: "io and sql threads running",
  1270  			in: &replicationdatapb.StopReplicationStatus{
  1271  				Before: &replicationdatapb.Status{
  1272  					IoState:  int32(mysql.ReplicationStateRunning),
  1273  					SqlState: int32(mysql.ReplicationStateRunning),
  1274  				},
  1275  			},
  1276  			expected:  true,
  1277  			shouldErr: false,
  1278  		},
  1279  		{
  1280  			name: "no replication threads running",
  1281  			in: &replicationdatapb.StopReplicationStatus{
  1282  				Before: &replicationdatapb.Status{
  1283  					IoState:  int32(mysql.ReplicationStateStopped),
  1284  					SqlState: int32(mysql.ReplicationStateStopped),
  1285  				},
  1286  			},
  1287  			expected:  false,
  1288  			shouldErr: false,
  1289  		},
  1290  		{
  1291  			name:      "passing nil pointer results in an error",
  1292  			in:        nil,
  1293  			expected:  false,
  1294  			shouldErr: true,
  1295  		},
  1296  		{
  1297  			name: "status.Before is nil results in an error",
  1298  			in: &replicationdatapb.StopReplicationStatus{
  1299  				Before: nil,
  1300  			},
  1301  			expected:  false,
  1302  			shouldErr: true,
  1303  		},
  1304  	}
  1305  
  1306  	for _, tt := range tests {
  1307  		tt := tt
  1308  
  1309  		t.Run(tt.name, func(t *testing.T) {
  1310  			t.Parallel()
  1311  
  1312  			actual, err := SQLThreadWasRunning(tt.in)
  1313  			if tt.shouldErr {
  1314  				assert.Error(t, err)
  1315  
  1316  				return
  1317  			}
  1318  
  1319  			assert.NoError(t, err)
  1320  			assert.Equal(t, tt.expected, actual)
  1321  		})
  1322  	}
  1323  }
  1324  
  1325  // waitForRelayLogsToApplyTestTMClient implements just the WaitForPosition
  1326  // method of the tmclient.TabletManagerClient interface for
  1327  // TestWaitForRelayLogsToApply, with the necessary trackers to facilitate
  1328  // testing that unit.
  1329  type waitForRelayLogsToApplyTestTMClient struct {
  1330  	tmclient.TabletManagerClient
  1331  	calledPositions []string
  1332  	shouldErr       bool
  1333  }
  1334  
  1335  func (fake *waitForRelayLogsToApplyTestTMClient) WaitForPosition(_ context.Context, _ *topodatapb.Tablet, position string) error {
  1336  	if fake.shouldErr {
  1337  		return assert.AnError
  1338  	}
  1339  
  1340  	fake.calledPositions = append(fake.calledPositions, position)
  1341  	return nil
  1342  }
  1343  
  1344  func TestWaitForRelayLogsToApply(t *testing.T) {
  1345  	t.Parallel()
  1346  
  1347  	ctx := context.Background()
  1348  	tests := []struct {
  1349  		name                    string
  1350  		client                  *waitForRelayLogsToApplyTestTMClient
  1351  		status                  *replicationdatapb.StopReplicationStatus
  1352  		expectedCalledPositions []string
  1353  		shouldErr               bool
  1354  	}{
  1355  		{
  1356  			name:   "using relay log position",
  1357  			client: &waitForRelayLogsToApplyTestTMClient{},
  1358  			status: &replicationdatapb.StopReplicationStatus{
  1359  				After: &replicationdatapb.Status{
  1360  					RelayLogPosition: "relay-pos",
  1361  				},
  1362  			},
  1363  			expectedCalledPositions: []string{"relay-pos"},
  1364  			shouldErr:               false,
  1365  		},
  1366  		{
  1367  			name:   "using file relay log position",
  1368  			client: &waitForRelayLogsToApplyTestTMClient{},
  1369  			status: &replicationdatapb.StopReplicationStatus{
  1370  				After: &replicationdatapb.Status{
  1371  					RelayLogSourceBinlogEquivalentPosition: "file-relay-pos",
  1372  				},
  1373  			},
  1374  			expectedCalledPositions: []string{"file-relay-pos"},
  1375  			shouldErr:               false,
  1376  		},
  1377  		{
  1378  			name:   "when both are set, relay log position takes precedence over file relay log position",
  1379  			client: &waitForRelayLogsToApplyTestTMClient{},
  1380  			status: &replicationdatapb.StopReplicationStatus{
  1381  				After: &replicationdatapb.Status{
  1382  					RelayLogPosition: "relay-pos",
  1383  					FilePosition:     "file-relay-pos",
  1384  				},
  1385  			},
  1386  			expectedCalledPositions: []string{"relay-pos"},
  1387  			shouldErr:               false,
  1388  		},
  1389  		{
  1390  			name: "error waiting for position",
  1391  			client: &waitForRelayLogsToApplyTestTMClient{
  1392  				shouldErr: true,
  1393  			},
  1394  			status: &replicationdatapb.StopReplicationStatus{
  1395  				After: &replicationdatapb.Status{
  1396  					RelayLogPosition: "relay-pos",
  1397  				},
  1398  			},
  1399  			expectedCalledPositions: nil,
  1400  			shouldErr:               true,
  1401  		},
  1402  	}
  1403  
  1404  	for _, tt := range tests {
  1405  		tt := tt
  1406  
  1407  		t.Run(tt.name, func(t *testing.T) {
  1408  			t.Parallel()
  1409  
  1410  			err := WaitForRelayLogsToApply(ctx, tt.client, &topo.TabletInfo{}, tt.status)
  1411  			defer assert.Equal(t, tt.expectedCalledPositions, tt.client.calledPositions)
  1412  			if tt.shouldErr {
  1413  				assert.Error(t, err)
  1414  				return
  1415  			}
  1416  
  1417  			assert.NoError(t, err)
  1418  		})
  1419  	}
  1420  }