vitess.io/vitess@v0.16.2/go/vt/vtorc/logic/tablet_discovery_test.go (about)

     1  /*
     2  Copyright 2022 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 logic
    18  
    19  import (
    20  	"context"
    21  	"sync/atomic"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	"google.golang.org/protobuf/proto"
    28  
    29  	"vitess.io/vitess/go/vt/external/golib/sqlutils"
    30  
    31  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    32  	"vitess.io/vitess/go/vt/proto/vttime"
    33  	"vitess.io/vitess/go/vt/topo/memorytopo"
    34  	"vitess.io/vitess/go/vt/vtorc/db"
    35  	"vitess.io/vitess/go/vt/vtorc/inst"
    36  )
    37  
    38  var (
    39  	keyspace = "ks"
    40  	shard    = "0"
    41  	hostname = "localhost"
    42  	cell1    = "zone-1"
    43  	tab100   = &topodatapb.Tablet{
    44  		Alias: &topodatapb.TabletAlias{
    45  			Cell: cell1,
    46  			Uid:  100,
    47  		},
    48  		Hostname:      hostname,
    49  		Keyspace:      keyspace,
    50  		Shard:         shard,
    51  		Type:          topodatapb.TabletType_PRIMARY,
    52  		MysqlHostname: hostname,
    53  		MysqlPort:     100,
    54  		PrimaryTermStartTime: &vttime.Time{
    55  			Seconds: 15,
    56  		},
    57  	}
    58  	tab101 = &topodatapb.Tablet{
    59  		Alias: &topodatapb.TabletAlias{
    60  			Cell: cell1,
    61  			Uid:  101,
    62  		},
    63  		Hostname:      hostname,
    64  		Keyspace:      keyspace,
    65  		Shard:         shard,
    66  		Type:          topodatapb.TabletType_REPLICA,
    67  		MysqlHostname: hostname,
    68  		MysqlPort:     101,
    69  	}
    70  	tab102 = &topodatapb.Tablet{
    71  		Alias: &topodatapb.TabletAlias{
    72  			Cell: cell1,
    73  			Uid:  102,
    74  		},
    75  		Hostname:      hostname,
    76  		Keyspace:      keyspace,
    77  		Shard:         shard,
    78  		Type:          topodatapb.TabletType_RDONLY,
    79  		MysqlHostname: hostname,
    80  		MysqlPort:     102,
    81  	}
    82  	tab103 = &topodatapb.Tablet{
    83  		Alias: &topodatapb.TabletAlias{
    84  			Cell: cell1,
    85  			Uid:  103,
    86  		},
    87  		Hostname:      hostname,
    88  		Keyspace:      keyspace,
    89  		Shard:         shard,
    90  		Type:          topodatapb.TabletType_PRIMARY,
    91  		MysqlHostname: hostname,
    92  		MysqlPort:     103,
    93  		PrimaryTermStartTime: &vttime.Time{
    94  			// Higher time than tab100
    95  			Seconds: 3500,
    96  		},
    97  	}
    98  )
    99  
   100  func TestRefreshTabletsInKeyspaceShard(t *testing.T) {
   101  	// Store the old flags and restore on test completion
   102  	oldTs := ts
   103  	defer func() {
   104  		ts = oldTs
   105  	}()
   106  
   107  	// Open the vtorc
   108  	// After the test completes delete everything from the vitess_tablet table
   109  	orcDb, err := db.OpenVTOrc()
   110  	require.NoError(t, err)
   111  	defer func() {
   112  		_, err = orcDb.Exec("delete from vitess_tablet")
   113  		require.NoError(t, err)
   114  	}()
   115  
   116  	// Create a memory topo-server and create the keyspace and shard records
   117  	ts = memorytopo.NewServer(cell1)
   118  	_, err = ts.GetOrCreateShard(context.Background(), keyspace, shard)
   119  	require.NoError(t, err)
   120  
   121  	// Add tablets to the topo-server
   122  	tablets := []*topodatapb.Tablet{tab100, tab101, tab102}
   123  	for _, tablet := range tablets {
   124  		err := ts.CreateTablet(context.Background(), tablet)
   125  		require.NoError(t, err)
   126  	}
   127  
   128  	t.Run("initial call to refreshTabletsInKeyspaceShard", func(t *testing.T) {
   129  		// We expect all 3 tablets to be refreshed since they are being discovered for the first time
   130  		verifyRefreshTabletsInKeyspaceShard(t, false, 3, tablets)
   131  	})
   132  
   133  	t.Run("call refreshTabletsInKeyspaceShard again - no force refresh", func(t *testing.T) {
   134  		// We expect no tablets to be refreshed since they are all already upto date
   135  		verifyRefreshTabletsInKeyspaceShard(t, false, 0, tablets)
   136  	})
   137  
   138  	t.Run("call refreshTabletsInKeyspaceShard again - force refresh", func(t *testing.T) {
   139  		// We expect all 3 tablets to be refreshed since we requested force refresh
   140  		verifyRefreshTabletsInKeyspaceShard(t, true, 3, tablets)
   141  	})
   142  
   143  	t.Run("tablet shutdown removes mysql hostname and port. We shouldn't forget the tablet", func(t *testing.T) {
   144  		defer func() {
   145  			_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   146  				tablet.MysqlHostname = hostname
   147  				tablet.MysqlPort = 100
   148  				return nil
   149  			})
   150  		}()
   151  		// Let's assume tab100 shutdown. This would clear its tablet hostname and port
   152  		_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   153  			tablet.MysqlHostname = ""
   154  			tablet.MysqlPort = 0
   155  			return nil
   156  		})
   157  		require.NoError(t, err)
   158  		// We expect no tablets to be refreshed. Also, tab100 shouldn't be forgotten
   159  		verifyRefreshTabletsInKeyspaceShard(t, false, 0, tablets)
   160  	})
   161  
   162  	t.Run("change a tablet and call refreshTabletsInKeyspaceShard again", func(t *testing.T) {
   163  		startTimeInitially := tab100.PrimaryTermStartTime.Seconds
   164  		defer func() {
   165  			tab100.PrimaryTermStartTime.Seconds = startTimeInitially
   166  			_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   167  				tablet.PrimaryTermStartTime.Seconds = startTimeInitially
   168  				return nil
   169  			})
   170  		}()
   171  		tab100.PrimaryTermStartTime.Seconds = 1000
   172  		_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   173  			tablet.PrimaryTermStartTime.Seconds = 1000
   174  			return nil
   175  		})
   176  		require.NoError(t, err)
   177  		// We expect 1 tablet to be refreshed since that is the only one that has changed
   178  		verifyRefreshTabletsInKeyspaceShard(t, false, 1, tablets)
   179  	})
   180  
   181  	t.Run("change the port and call refreshTabletsInKeyspaceShard again", func(t *testing.T) {
   182  		defer func() {
   183  			_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   184  				tablet.MysqlPort = 100
   185  				return nil
   186  			})
   187  			tab100.MysqlPort = 100
   188  		}()
   189  		// Let's assume tab100 restarted on a different pod. This would change its tablet hostname and port
   190  		_, err = ts.UpdateTabletFields(context.Background(), tab100.Alias, func(tablet *topodatapb.Tablet) error {
   191  			tablet.MysqlPort = 39293
   192  			return nil
   193  		})
   194  		require.NoError(t, err)
   195  		tab100.MysqlPort = 39293
   196  		// We expect 1 tablet to be refreshed since that is the only one that has changed
   197  		// Also the old tablet should be forgotten
   198  		verifyRefreshTabletsInKeyspaceShard(t, false, 1, tablets)
   199  	})
   200  }
   201  
   202  func TestShardPrimary(t *testing.T) {
   203  	testcases := []*struct {
   204  		name            string
   205  		tablets         []*topodatapb.Tablet
   206  		expectedPrimary *topodatapb.Tablet
   207  		expectedErr     string
   208  	}{
   209  		{
   210  			name:            "One primary type tablet",
   211  			tablets:         []*topodatapb.Tablet{tab100, tab101, tab102},
   212  			expectedPrimary: tab100,
   213  		}, {
   214  			name:    "Two primary type tablets",
   215  			tablets: []*topodatapb.Tablet{tab100, tab101, tab102, tab103},
   216  			// In this case we expect the tablet with higher PrimaryTermStartTime to be the primary tablet
   217  			expectedPrimary: tab103,
   218  		}, {
   219  			name:        "No primary type tablets",
   220  			tablets:     []*topodatapb.Tablet{tab101, tab102},
   221  			expectedErr: "no primary tablet found",
   222  		},
   223  	}
   224  
   225  	oldTs := ts
   226  	defer func() {
   227  		ts = oldTs
   228  	}()
   229  
   230  	// Open the vtorc
   231  	// After the test completes delete everything from the vitess_tablet table
   232  	orcDb, err := db.OpenVTOrc()
   233  	require.NoError(t, err)
   234  	defer func() {
   235  		_, err = orcDb.Exec("delete from vitess_tablet")
   236  		require.NoError(t, err)
   237  	}()
   238  
   239  	for _, testcase := range testcases {
   240  		t.Run(testcase.name, func(t *testing.T) {
   241  			_, err = orcDb.Exec("delete from vitess_tablet")
   242  
   243  			// Create a memory topo-server and create the keyspace and shard records
   244  			ts = memorytopo.NewServer(cell1)
   245  			_, err = ts.GetOrCreateShard(context.Background(), keyspace, shard)
   246  			require.NoError(t, err)
   247  
   248  			// Add tablets to the topo-server
   249  			for _, tablet := range testcase.tablets {
   250  				err := ts.CreateTablet(context.Background(), tablet)
   251  				require.NoError(t, err)
   252  			}
   253  
   254  			// refresh the tablet info so that they are stored in the orch backend
   255  			verifyRefreshTabletsInKeyspaceShard(t, false, len(testcase.tablets), testcase.tablets)
   256  
   257  			primary, err := shardPrimary(keyspace, shard)
   258  			if testcase.expectedErr != "" {
   259  				assert.Contains(t, err.Error(), testcase.expectedErr)
   260  				assert.Nil(t, primary)
   261  			} else {
   262  				assert.NoError(t, err)
   263  				diff := cmp.Diff(primary, testcase.expectedPrimary, cmp.Comparer(proto.Equal))
   264  				assert.Empty(t, diff)
   265  			}
   266  		})
   267  	}
   268  }
   269  
   270  // verifyRefreshTabletsInKeyspaceShard calls refreshTabletsInKeyspaceShard with the forceRefresh parameter provided and verifies that
   271  // the number of instances refreshed matches the parameter and all the tablets match the ones provided
   272  func verifyRefreshTabletsInKeyspaceShard(t *testing.T, forceRefresh bool, instanceRefreshRequired int, tablets []*topodatapb.Tablet) {
   273  	var instancesRefreshed atomic.Int32
   274  	instancesRefreshed.Store(0)
   275  	// call refreshTabletsInKeyspaceShard while counting all the instances that are refreshed
   276  	refreshTabletsInKeyspaceShard(context.Background(), keyspace, shard, func(instanceKey *inst.InstanceKey) {
   277  		instancesRefreshed.Add(1)
   278  	}, forceRefresh)
   279  	// Verify that all the tablets are present in the database
   280  	for _, tablet := range tablets {
   281  		verifyTabletInfo(t, tablet, "")
   282  	}
   283  	verifyTabletCount(t, len(tablets))
   284  	// Verify that refresh as many tablets as expected
   285  	assert.EqualValues(t, instanceRefreshRequired, instancesRefreshed.Load())
   286  }
   287  
   288  // verifyTabletInfo verifies that the tablet information read from the vtorc database
   289  // is the same as the one provided or reading it gives the same error as expected
   290  func verifyTabletInfo(t *testing.T, tabletWanted *topodatapb.Tablet, errString string) {
   291  	t.Helper()
   292  	tabletKey := inst.InstanceKey{
   293  		Hostname: hostname,
   294  		Port:     int(tabletWanted.MysqlPort),
   295  	}
   296  	tablet, err := inst.ReadTablet(tabletKey)
   297  	if errString != "" {
   298  		assert.EqualError(t, err, errString)
   299  	} else {
   300  		assert.NoError(t, err)
   301  		assert.EqualValues(t, tabletKey.Port, tablet.MysqlPort)
   302  		diff := cmp.Diff(tablet, tabletWanted, cmp.Comparer(proto.Equal))
   303  		assert.Empty(t, diff)
   304  	}
   305  }
   306  
   307  // verifyTabletCount verifies that the number of tablets in the vitess_tablet table match the given count
   308  func verifyTabletCount(t *testing.T, countWanted int) {
   309  	t.Helper()
   310  	totalTablets := 0
   311  	err := db.QueryVTOrc("select count(*) as total_tablets from vitess_tablet", nil, func(rowMap sqlutils.RowMap) error {
   312  		totalTablets = rowMap.GetInt("total_tablets")
   313  		return nil
   314  	})
   315  	require.NoError(t, err)
   316  	require.Equal(t, countWanted, totalTablets)
   317  }