vitess.io/vitess@v0.16.2/go/test/endtoend/tabletmanager/tablegc/tablegc_test.go (about) 1 /* 2 Copyright 2020 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 package tablegc 17 18 import ( 19 "context" 20 "flag" 21 "os" 22 "testing" 23 "time" 24 25 "vitess.io/vitess/go/mysql" 26 "vitess.io/vitess/go/vt/schema" 27 "vitess.io/vitess/go/vt/sqlparser" 28 29 "vitess.io/vitess/go/test/endtoend/cluster" 30 "vitess.io/vitess/go/test/endtoend/onlineddl" 31 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/require" 34 ) 35 36 var ( 37 clusterInstance *cluster.LocalProcessCluster 38 primaryTablet cluster.Vttablet 39 hostname = "localhost" 40 keyspaceName = "ks" 41 cell = "zone1" 42 fastDropTable bool 43 sqlCreateTable = ` 44 create table if not exists t1( 45 id bigint not null auto_increment, 46 value varchar(32), 47 primary key(id) 48 ) Engine=InnoDB; 49 ` 50 sqlCreateView = ` 51 create or replace view v1 as select * from t1; 52 ` 53 sqlSchema = sqlCreateTable + sqlCreateView 54 55 vSchema = ` 56 { 57 "sharded": true, 58 "vindexes": { 59 "hash": { 60 "type": "hash" 61 } 62 }, 63 "tables": { 64 "t1": { 65 "column_vindexes": [ 66 { 67 "column": "id", 68 "name": "hash" 69 } 70 ] 71 } 72 } 73 }` 74 75 tableTransitionExpiration = 10 * time.Second 76 gcCheckInterval = 2 * time.Second 77 gcPurgeCheckInterval = 2 * time.Second 78 waitForTransitionTimeout = 30 * time.Second 79 ) 80 81 func TestMain(m *testing.M) { 82 defer cluster.PanicHandler(nil) 83 flag.Parse() 84 85 exitCode := func() int { 86 clusterInstance = cluster.NewCluster(cell, hostname) 87 defer clusterInstance.Teardown() 88 89 // Start topo server 90 err := clusterInstance.StartTopo() 91 if err != nil { 92 return 1 93 } 94 95 // Set extra tablet args for lock timeout 96 clusterInstance.VtTabletExtraArgs = []string{ 97 "--lock_tables_timeout", "5s", 98 "--watch_replication_stream", 99 "--enable_replication_reporter", 100 "--heartbeat_enable", 101 "--heartbeat_interval", "250ms", 102 "--gc_check_interval", gcCheckInterval.String(), 103 "--gc_purge_check_interval", gcPurgeCheckInterval.String(), 104 "--table_gc_lifecycle", "hold,purge,evac,drop", 105 } 106 107 // Start keyspace 108 keyspace := &cluster.Keyspace{ 109 Name: keyspaceName, 110 SchemaSQL: sqlSchema, 111 VSchema: vSchema, 112 } 113 114 if err = clusterInstance.StartUnshardedKeyspace(*keyspace, 1, false); err != nil { 115 return 1 116 } 117 118 // Collect table paths and ports 119 tablets := clusterInstance.Keyspaces[0].Shards[0].Vttablets 120 for _, tablet := range tablets { 121 if tablet.Type == "primary" { 122 primaryTablet = *tablet 123 } 124 } 125 126 return m.Run() 127 }() 128 os.Exit(exitCode) 129 } 130 131 func checkTableRows(t *testing.T, tableName string, expect int64) { 132 require.NotEmpty(t, tableName) 133 query := `select count(*) as c from %a` 134 parsed := sqlparser.BuildParsedQuery(query, tableName) 135 rs, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true) 136 require.NoError(t, err) 137 count := rs.Named().Row().AsInt64("c", 0) 138 assert.Equal(t, expect, count) 139 } 140 141 func populateTable(t *testing.T) { 142 _, err := primaryTablet.VttabletProcess.QueryTablet(sqlSchema, keyspaceName, true) 143 require.NoError(t, err) 144 _, err = primaryTablet.VttabletProcess.QueryTablet("delete from t1", keyspaceName, true) 145 require.NoError(t, err) 146 _, err = primaryTablet.VttabletProcess.QueryTablet("insert into t1 (id, value) values (null, md5(rand()))", keyspaceName, true) 147 require.NoError(t, err) 148 for i := 0; i < 10; i++ { 149 _, err = primaryTablet.VttabletProcess.QueryTablet("insert into t1 (id, value) select null, md5(rand()) from t1", keyspaceName, true) 150 require.NoError(t, err) 151 } 152 checkTableRows(t, "t1", 1024) 153 { 154 exists, _, err := tableExists("t1") 155 require.NoError(t, err) 156 require.True(t, exists) 157 } 158 } 159 160 // tableExists sees that a given table exists in MySQL 161 func tableExists(tableExpr string) (exists bool, tableName string, err error) { 162 query := `select table_name as table_name from information_schema.tables where table_schema=database() and table_name like '%a'` 163 parsed := sqlparser.BuildParsedQuery(query, tableExpr) 164 rs, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true) 165 if err != nil { 166 return false, "", err 167 } 168 row := rs.Named().Row() 169 if row == nil { 170 return false, "", nil 171 } 172 return true, row.AsString("table_name", ""), nil 173 } 174 175 func validateTableDoesNotExist(t *testing.T, tableExpr string) { 176 ctx, cancel := context.WithTimeout(context.Background(), waitForTransitionTimeout) 177 defer cancel() 178 179 ticker := time.NewTicker(time.Second) 180 var foundTableName string 181 var exists bool 182 var err error 183 for { 184 select { 185 case <-ticker.C: 186 exists, foundTableName, err = tableExists(tableExpr) 187 require.NoError(t, err) 188 if !exists { 189 return 190 } 191 case <-ctx.Done(): 192 assert.NoError(t, ctx.Err(), "validateTableDoesNotExist timed out, table %v still exists (%v)", tableExpr, foundTableName) 193 return 194 } 195 } 196 } 197 198 func validateTableExists(t *testing.T, tableExpr string) { 199 ctx, cancel := context.WithTimeout(context.Background(), waitForTransitionTimeout) 200 defer cancel() 201 202 ticker := time.NewTicker(time.Second) 203 var exists bool 204 var err error 205 for { 206 select { 207 case <-ticker.C: 208 exists, _, err = tableExists(tableExpr) 209 require.NoError(t, err) 210 if exists { 211 return 212 } 213 case <-ctx.Done(): 214 assert.NoError(t, ctx.Err(), "validateTableExists timed out, table %v still does not exist", tableExpr) 215 return 216 } 217 } 218 } 219 220 func validateAnyState(t *testing.T, expectNumRows int64, states ...schema.TableGCState) { 221 for _, state := range states { 222 expectTableToExist := true 223 searchExpr := "" 224 switch state { 225 case schema.HoldTableGCState: 226 searchExpr = `\_vt\_HOLD\_%` 227 case schema.PurgeTableGCState: 228 searchExpr = `\_vt\_PURGE\_%` 229 case schema.EvacTableGCState: 230 searchExpr = `\_vt\_EVAC\_%` 231 case schema.DropTableGCState: 232 searchExpr = `\_vt\_DROP\_%` 233 case schema.TableDroppedGCState: 234 searchExpr = `\_vt\_%` 235 expectTableToExist = false 236 default: 237 t.Log("Unknown state") 238 t.Fail() 239 } 240 exists, tableName, err := tableExists(searchExpr) 241 require.NoError(t, err) 242 243 if exists { 244 if expectNumRows >= 0 { 245 checkTableRows(t, tableName, expectNumRows) 246 } 247 // Now that the table is validated, we can drop it 248 dropTable(t, tableName) 249 } 250 if exists == expectTableToExist { 251 // condition met 252 return 253 } 254 } 255 assert.Fail(t, "could not match any of the states: %v", states) 256 } 257 258 // dropTable drops a table 259 func dropTable(t *testing.T, tableName string) { 260 query := `drop table if exists %a` 261 parsed := sqlparser.BuildParsedQuery(query, tableName) 262 _, err := primaryTablet.VttabletProcess.QueryTablet(parsed.Query, keyspaceName, true) 263 require.NoError(t, err) 264 } 265 266 func TestCapability(t *testing.T) { 267 mysqlVersion := onlineddl.GetMySQLVersion(t, clusterInstance.Keyspaces[0].Shards[0].PrimaryTablet()) 268 require.NotEmpty(t, mysqlVersion) 269 270 _, capableOf, _ := mysql.GetFlavor(mysqlVersion, nil) 271 require.NotNil(t, capableOf) 272 var err error 273 fastDropTable, err = capableOf(mysql.FastDropTableFlavorCapability) 274 require.NoError(t, err) 275 } 276 277 func TestPopulateTable(t *testing.T) { 278 populateTable(t) 279 validateTableExists(t, "t1") 280 validateTableDoesNotExist(t, "no_such_table") 281 } 282 283 func TestHold(t *testing.T) { 284 populateTable(t) 285 query, tableName, err := schema.GenerateRenameStatement("t1", schema.HoldTableGCState, time.Now().UTC().Add(tableTransitionExpiration)) 286 assert.NoError(t, err) 287 288 _, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 289 assert.NoError(t, err) 290 291 validateTableDoesNotExist(t, "t1") 292 validateTableExists(t, tableName) 293 294 time.Sleep(tableTransitionExpiration / 2) 295 { 296 // Table was created with +10s timestamp, so it should still exist 297 validateTableExists(t, tableName) 298 299 checkTableRows(t, tableName, 1024) 300 } 301 302 time.Sleep(tableTransitionExpiration) 303 // We're now both beyond table's timestamp as well as a tableGC interval 304 validateTableDoesNotExist(t, tableName) 305 if fastDropTable { 306 validateAnyState(t, -1, schema.DropTableGCState, schema.TableDroppedGCState) 307 } else { 308 validateAnyState(t, -1, schema.PurgeTableGCState, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState) 309 } 310 } 311 312 func TestEvac(t *testing.T) { 313 populateTable(t) 314 query, tableName, err := schema.GenerateRenameStatement("t1", schema.EvacTableGCState, time.Now().UTC().Add(tableTransitionExpiration)) 315 assert.NoError(t, err) 316 317 _, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 318 assert.NoError(t, err) 319 320 validateTableDoesNotExist(t, "t1") 321 322 time.Sleep(tableTransitionExpiration / 2) 323 { 324 // Table was created with +10s timestamp, so it should still exist 325 if fastDropTable { 326 // EVAC state is skipped in mysql 8.0.23 and beyond 327 validateTableDoesNotExist(t, tableName) 328 } else { 329 validateTableExists(t, tableName) 330 checkTableRows(t, tableName, 1024) 331 } 332 } 333 334 time.Sleep(tableTransitionExpiration) 335 // We're now both beyond table's timestamp as well as a tableGC interval 336 validateTableDoesNotExist(t, tableName) 337 // Table should be renamed as _vt_DROP_... and then dropped! 338 validateAnyState(t, 0, schema.DropTableGCState, schema.TableDroppedGCState) 339 } 340 341 func TestDrop(t *testing.T) { 342 populateTable(t) 343 query, tableName, err := schema.GenerateRenameStatement("t1", schema.DropTableGCState, time.Now().UTC().Add(tableTransitionExpiration)) 344 assert.NoError(t, err) 345 346 _, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 347 assert.NoError(t, err) 348 349 validateTableDoesNotExist(t, "t1") 350 351 time.Sleep(tableTransitionExpiration) 352 time.Sleep(2 * gcCheckInterval) 353 // We're now both beyond table's timestamp as well as a tableGC interval 354 validateTableDoesNotExist(t, tableName) 355 } 356 357 func TestPurge(t *testing.T) { 358 populateTable(t) 359 query, tableName, err := schema.GenerateRenameStatement("t1", schema.PurgeTableGCState, time.Now().UTC().Add(tableTransitionExpiration)) 360 require.NoError(t, err) 361 362 _, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 363 require.NoError(t, err) 364 365 validateTableDoesNotExist(t, "t1") 366 if !fastDropTable { 367 validateTableExists(t, tableName) 368 checkTableRows(t, tableName, 1024) 369 } 370 time.Sleep(5 * gcPurgeCheckInterval) // wwait for table to be purged 371 time.Sleep(2 * gcCheckInterval) // wait for GC state transition 372 if fastDropTable { 373 validateAnyState(t, 0, schema.DropTableGCState, schema.TableDroppedGCState) 374 } else { 375 validateAnyState(t, 0, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState) 376 } 377 } 378 379 func TestPurgeView(t *testing.T) { 380 populateTable(t) 381 query, tableName, err := schema.GenerateRenameStatement("v1", schema.PurgeTableGCState, time.Now().UTC().Add(tableTransitionExpiration)) 382 require.NoError(t, err) 383 384 _, err = primaryTablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 385 require.NoError(t, err) 386 387 // table untouched 388 validateTableExists(t, "t1") 389 if !fastDropTable { 390 validateTableExists(t, tableName) 391 } 392 validateTableDoesNotExist(t, "v1") 393 394 time.Sleep(tableTransitionExpiration / 2) 395 { 396 // View was created with +10s timestamp, so it should still exist 397 if fastDropTable { 398 // PURGE is skipped in mysql 8.0.23 399 validateTableDoesNotExist(t, tableName) 400 } else { 401 validateTableExists(t, tableName) 402 // We're really reading the view here: 403 checkTableRows(t, tableName, 1024) 404 } 405 } 406 407 time.Sleep(2 * gcPurgeCheckInterval) // wwait for table to be purged 408 time.Sleep(2 * gcCheckInterval) // wait for GC state transition 409 410 // We're now both beyond view's timestamp as well as a tableGC interval 411 validateTableDoesNotExist(t, tableName) 412 // table still untouched 413 validateTableExists(t, "t1") 414 validateAnyState(t, 1024, schema.EvacTableGCState, schema.DropTableGCState, schema.TableDroppedGCState) 415 }