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 }