vitess.io/vitess@v0.16.2/go/test/endtoend/onlineddl/ghost/onlineddl_ghost_test.go (about) 1 /* 2 Copyright 2019 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 ghost 18 19 import ( 20 "flag" 21 "fmt" 22 "os" 23 "path" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 "vitess.io/vitess/go/mysql" 30 "vitess.io/vitess/go/vt/schema" 31 32 "vitess.io/vitess/go/test/endtoend/cluster" 33 "vitess.io/vitess/go/test/endtoend/onlineddl" 34 35 "github.com/stretchr/testify/assert" 36 "github.com/stretchr/testify/require" 37 ) 38 39 var ( 40 clusterInstance *cluster.LocalProcessCluster 41 shards []cluster.Shard 42 vtParams mysql.ConnParams 43 hostname = "localhost" 44 keyspaceName = "ks" 45 cell = "zone1" 46 schemaChangeDirectory = "" 47 totalTableCount = 4 48 49 normalMigrationWait = 20 * time.Second 50 51 createTable = ` 52 CREATE TABLE %s ( 53 id bigint(20) NOT NULL, 54 msg varchar(64), 55 PRIMARY KEY (id) 56 ) ENGINE=InnoDB;` 57 insertStatements = []string{ 58 `insert into %s (id, msg) values (3, 'three')`, 59 `insert into %s (id, msg) values (5, 'five')`, 60 `insert into %s (id, msg) values (7, 'seven')`, 61 `insert into %s (id, msg) values (11, 'eleven')`, 62 `insert into %s (id, msg) values (13, 'thirteen')`, 63 } 64 // To verify non online-DDL behavior 65 alterTableNormalStatement = ` 66 ALTER TABLE %s 67 ADD COLUMN non_online int UNSIGNED NOT NULL` 68 // A trivial statement which must succeed and does not change the schema 69 alterTableTrivialStatement = ` 70 ALTER TABLE %s 71 ENGINE=InnoDB` 72 // The following statement is valid 73 alterTableSuccessfulStatement = ` 74 ALTER TABLE %s 75 MODIFY id bigint UNSIGNED NOT NULL, 76 ADD COLUMN ghost_col int NOT NULL, 77 ADD INDEX idx_msg(msg)` 78 // The following statement will fail because gh-ost requires some shared unique key 79 alterTableFailedStatement = ` 80 ALTER TABLE %s 81 DROP PRIMARY KEY, 82 DROP COLUMN ghost_col` 83 // We will run this query with "gh-ost --max-load=Threads_running=1" 84 alterTableThrottlingStatement = ` 85 ALTER TABLE %s 86 DROP COLUMN ghost_col` 87 onlineDDLCreateTableStatement = ` 88 CREATE TABLE %s ( 89 id bigint NOT NULL, 90 online_ddl_create_col INT NOT NULL, 91 PRIMARY KEY (id) 92 ) ENGINE=InnoDB;` 93 noPKCreateTableStatement = ` 94 CREATE TABLE %s ( 95 online_ddl_create_col INT NOT NULL 96 ) ENGINE=InnoDB;` 97 onlineDDLDropTableStatement = ` 98 DROP TABLE %s` 99 onlineDDLDropTableIfExistsStatement = ` 100 DROP TABLE IF EXISTS %s` 101 102 vSchema = ` 103 { 104 "sharded": true, 105 "vindexes": { 106 "hash_index": { 107 "type": "hash" 108 } 109 }, 110 "tables": { 111 "vt_onlineddl_test_00": { 112 "column_vindexes": [ 113 { 114 "column": "id", 115 "name": "hash_index" 116 } 117 ] 118 }, 119 "vt_onlineddl_test_01": { 120 "column_vindexes": [ 121 { 122 "column": "id", 123 "name": "hash_index" 124 } 125 ] 126 }, 127 "vt_onlineddl_test_02": { 128 "column_vindexes": [ 129 { 130 "column": "id", 131 "name": "hash_index" 132 } 133 ] 134 }, 135 "vt_onlineddl_test_03": { 136 "column_vindexes": [ 137 { 138 "column": "id", 139 "name": "hash_index" 140 } 141 ] 142 } 143 } 144 } 145 ` 146 ) 147 148 func TestMain(m *testing.M) { 149 defer cluster.PanicHandler(nil) 150 flag.Parse() 151 152 exitcode, err := func() (int, error) { 153 clusterInstance = cluster.NewCluster(cell, hostname) 154 schemaChangeDirectory = path.Join("/tmp", fmt.Sprintf("schema_change_dir_%d", clusterInstance.GetAndReserveTabletUID())) 155 defer os.RemoveAll(schemaChangeDirectory) 156 defer clusterInstance.Teardown() 157 158 if _, err := os.Stat(schemaChangeDirectory); os.IsNotExist(err) { 159 _ = os.Mkdir(schemaChangeDirectory, 0700) 160 } 161 162 clusterInstance.VtctldExtraArgs = []string{ 163 "--schema_change_dir", schemaChangeDirectory, 164 "--schema_change_controller", "local", 165 "--schema_change_check_interval", "1", 166 } 167 168 clusterInstance.VtTabletExtraArgs = []string{ 169 "--enable-lag-throttler", 170 "--throttle_threshold", "1s", 171 "--heartbeat_enable", 172 "--heartbeat_interval", "250ms", 173 "--heartbeat_on_demand_duration", "5s", 174 "--migration_check_interval", "5s", 175 "--gh-ost-path", os.Getenv("VITESS_ENDTOEND_GH_OST_PATH"), // leave env variable empty/unset to get the default behavior. Override in Mac. 176 } 177 clusterInstance.VtGateExtraArgs = []string{ 178 "--ddl_strategy", "gh-ost", 179 } 180 181 if err := clusterInstance.StartTopo(); err != nil { 182 return 1, err 183 } 184 185 keyspace := &cluster.Keyspace{ 186 Name: keyspaceName, 187 VSchema: vSchema, 188 } 189 190 if err := clusterInstance.StartKeyspace(*keyspace, []string{"-80", "80-"}, 1, false); err != nil { 191 return 1, err 192 } 193 194 vtgateInstance := clusterInstance.NewVtgateInstance() 195 // Start vtgate 196 if err := vtgateInstance.Setup(); err != nil { 197 return 1, err 198 } 199 // ensure it is torn down during cluster TearDown 200 clusterInstance.VtgateProcess = *vtgateInstance 201 vtParams = mysql.ConnParams{ 202 Host: clusterInstance.Hostname, 203 Port: clusterInstance.VtgateMySQLPort, 204 } 205 206 return m.Run(), nil 207 }() 208 if err != nil { 209 fmt.Printf("%v\n", err) 210 os.Exit(1) 211 } else { 212 os.Exit(exitcode) 213 } 214 215 } 216 217 func TestSchemaChange(t *testing.T) { 218 defer cluster.PanicHandler(t) 219 shards = clusterInstance.Keyspaces[0].Shards 220 assert.Equal(t, 2, len(shards)) 221 testWithInitialSchema(t) 222 t.Run("create non_online", func(t *testing.T) { 223 _ = testOnlineDDLStatement(t, alterTableNormalStatement, string(schema.DDLStrategyDirect), "vtctl", "non_online", "") 224 }) 225 t.Run("successful online alter, vtgate", func(t *testing.T) { 226 uuid := testOnlineDDLStatement(t, alterTableSuccessfulStatement, "gh-ost", "vtgate", "ghost_col", "") 227 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 228 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 229 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 230 231 var totalRowsCopied uint64 232 // count sum of rows copied in all shards, that should be the total number of rows inserted to the table 233 rs := onlineddl.ReadMigrations(t, &vtParams, uuid) 234 require.NotNil(t, rs) 235 for _, row := range rs.Named().Rows { 236 rowsCopied := row.AsUint64("rows_copied", 0) 237 totalRowsCopied += rowsCopied 238 } 239 require.Equal(t, uint64(len(insertStatements)), totalRowsCopied) 240 241 // See that we're able to read logs after successful migration: 242 expectedMessage := "starting gh-ost" 243 logs := onlineddl.ReadMigrationLogs(t, &vtParams, uuid) 244 assert.Equal(t, len(shards), len(logs)) 245 for i := range logs { 246 require.Contains(t, logs[i], expectedMessage) 247 } 248 249 }) 250 t.Run("successful online alter, vtctl", func(t *testing.T) { 251 uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost", "vtctl", "ghost_col", "") 252 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 253 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 254 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 255 }) 256 t.Run("successful online alter, postponed, vtgate", func(t *testing.T) { 257 uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost -postpone-completion", "vtgate", "ghost_col", "") 258 // Should be still running! 259 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusRunning) 260 // Issue a complete and wait for successful completion 261 onlineddl.CheckCompleteMigration(t, &vtParams, shards, uuid, true) 262 // This part may take a while, because we depend on vreplicatoin polling 263 status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed) 264 fmt.Printf("# Migration status (for debug purposes): <%s>\n", status) 265 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 266 267 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 268 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 269 }) 270 t.Run("throttled migration", func(t *testing.T) { 271 uuid := testOnlineDDLStatement(t, alterTableThrottlingStatement, "gh-ost --max-load=Threads_running=1", "vtgate", "ghost_col", "") 272 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusRunning) 273 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, true) 274 status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, 20*time.Second, schema.OnlineDDLStatusFailed, schema.OnlineDDLStatusCancelled) 275 fmt.Printf("# Migration status (for debug purposes): <%s>\n", status) 276 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusCancelled) 277 }) 278 t.Run("failed migration", func(t *testing.T) { 279 uuid := testOnlineDDLStatement(t, alterTableFailedStatement, "gh-ost", "vtgate", "ghost_col", "") 280 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed) 281 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 282 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, true) 283 // migration will fail again 284 }) 285 t.Run("cancel all migrations: nothing to cancel", func(t *testing.T) { 286 // no migrations pending at this time 287 time.Sleep(10 * time.Second) 288 onlineddl.CheckCancelAllMigrations(t, &vtParams, 0) 289 }) 290 t.Run("cancel all migrations: some migrations to cancel", func(t *testing.T) { 291 // spawn n migrations; cancel them via cancel-all 292 var wg sync.WaitGroup 293 count := 4 294 for i := 0; i < count; i++ { 295 wg.Add(1) 296 go func() { 297 defer wg.Done() 298 _ = testOnlineDDLStatement(t, alterTableThrottlingStatement, "gh-ost --max-load=Threads_running=1", "vtgate", "ghost_col", "") 299 }() 300 } 301 wg.Wait() 302 onlineddl.CheckCancelAllMigrations(t, &vtParams, len(shards)*count) 303 }) 304 t.Run("Online DROP, vtctl", func(t *testing.T) { 305 uuid := testOnlineDDLStatement(t, onlineDDLDropTableStatement, "gh-ost", "vtctl", "", "") 306 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 307 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 308 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 309 }) 310 t.Run("Online CREATE, vtctl", func(t *testing.T) { 311 uuid := testOnlineDDLStatement(t, onlineDDLCreateTableStatement, "gh-ost", "vtctl", "online_ddl_create_col", "") 312 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 313 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 314 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 315 }) 316 t.Run("Online DROP TABLE IF EXISTS, vtgate", func(t *testing.T) { 317 uuid := testOnlineDDLStatement(t, onlineDDLDropTableIfExistsStatement, "gh-ost", "vtgate", "", "") 318 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 319 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 320 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 321 // this table existed 322 checkTables(t, schema.OnlineDDLToGCUUID(uuid), 1) 323 }) 324 t.Run("Online DROP TABLE IF EXISTS for nonexistent table, vtgate", func(t *testing.T) { 325 uuid := testOnlineDDLStatement(t, onlineDDLDropTableIfExistsStatement, "gh-ost", "vtgate", "", "") 326 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 327 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 328 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 329 // this table did not exist 330 checkTables(t, schema.OnlineDDLToGCUUID(uuid), 0) 331 }) 332 t.Run("Online DROP TABLE for nonexistent table, expect error, vtgate", func(t *testing.T) { 333 uuid := testOnlineDDLStatement(t, onlineDDLDropTableStatement, "gh-ost", "vtgate", "", "") 334 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed) 335 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 336 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, true) 337 }) 338 t.Run("Online CREATE no PK table, vtgate", func(t *testing.T) { 339 uuid := testOnlineDDLStatement(t, noPKCreateTableStatement, "gh-ost", "vtgate", "online_ddl_create_col", "") 340 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusComplete) 341 onlineddl.CheckCancelMigration(t, &vtParams, shards, uuid, false) 342 onlineddl.CheckRetryMigration(t, &vtParams, shards, uuid, false) 343 }) 344 t.Run("Fail ALTER for no PK table, vtgate", func(t *testing.T) { 345 uuid := testOnlineDDLStatement(t, alterTableTrivialStatement, "gh-ost", "vtgate", "", "") 346 onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, schema.OnlineDDLStatusFailed) 347 348 expectedMessage := "No PRIMARY nor UNIQUE key found" 349 rs := onlineddl.ReadMigrations(t, &vtParams, uuid) 350 require.NotNil(t, rs) 351 for _, row := range rs.Named().Rows { 352 message := row["message"].ToString() 353 // the following message is generated by gh-ost. We test that it is captured in our 'message' column: 354 require.Contains(t, message, expectedMessage) 355 } 356 357 // See that we're able to read logs after failed migration: 358 logs := onlineddl.ReadMigrationLogs(t, &vtParams, uuid) 359 assert.Equal(t, len(shards), len(logs)) 360 for i := range logs { 361 require.Contains(t, logs[i], expectedMessage) 362 } 363 }) 364 } 365 366 func testWithInitialSchema(t *testing.T) { 367 // Create 4 tables and populate them 368 var sqlQuery = "" //nolint 369 for i := 0; i < totalTableCount; i++ { 370 tableName := fmt.Sprintf("vt_onlineddl_test_%02d", i) 371 sqlQuery = fmt.Sprintf(createTable, tableName) 372 err := clusterInstance.VtctlclientProcess.ApplySchema(keyspaceName, sqlQuery) 373 require.Nil(t, err) 374 375 for _, insert := range insertStatements { 376 insertQuery := fmt.Sprintf(insert, tableName) 377 r := onlineddl.VtgateExecQuery(t, &vtParams, insertQuery, "") 378 require.NotNil(t, r) 379 } 380 } 381 382 // Check if 4 tables are created 383 checkTables(t, "", totalTableCount) 384 } 385 386 // testOnlineDDLStatement runs an online DDL, ALTER statement 387 func testOnlineDDLStatement(t *testing.T, alterStatement string, ddlStrategy string, executeStrategy string, expectHint string, callerID string) (uuid string) { 388 tableName := fmt.Sprintf("vt_onlineddl_test_%02d", 3) 389 sqlQuery := fmt.Sprintf(alterStatement, tableName) 390 if executeStrategy == "vtgate" { 391 row := onlineddl.VtgateExecDDL(t, &vtParams, ddlStrategy, sqlQuery, "").Named().Row() 392 if row != nil { 393 uuid = row.AsString("uuid", "") 394 } 395 } else { 396 var err error 397 uuid, err = clusterInstance.VtctlclientProcess.ApplySchemaWithOutput(keyspaceName, sqlQuery, cluster.VtctlClientParams{DDLStrategy: ddlStrategy, CallerID: callerID}) 398 assert.NoError(t, err) 399 } 400 uuid = strings.TrimSpace(uuid) 401 fmt.Println("# Generated UUID (for debug purposes):") 402 fmt.Printf("<%s>\n", uuid) 403 404 strategySetting, err := schema.ParseDDLStrategy(ddlStrategy) 405 assert.NoError(t, err) 406 407 if !strategySetting.Strategy.IsDirect() { 408 status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, normalMigrationWait, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed) 409 fmt.Printf("# Migration status (for debug purposes): <%s>\n", status) 410 } 411 412 if expectHint != "" { 413 checkMigratedTable(t, tableName, expectHint) 414 } 415 return uuid 416 } 417 418 // checkTables checks the number of tables in the first two shards. 419 func checkTables(t *testing.T, showTableName string, expectCount int) { 420 for i := range clusterInstance.Keyspaces[0].Shards { 421 checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], showTableName, expectCount) 422 } 423 } 424 425 // checkTablesCount checks the number of tables in the given tablet 426 func checkTablesCount(t *testing.T, tablet *cluster.Vttablet, showTableName string, expectCount int) { 427 query := fmt.Sprintf(`show tables like '%%%s%%';`, showTableName) 428 queryResult, err := tablet.VttabletProcess.QueryTablet(query, keyspaceName, true) 429 require.Nil(t, err) 430 assert.Equal(t, expectCount, len(queryResult.Rows)) 431 } 432 433 // checkMigratedTables checks the CREATE STATEMENT of a table after migration 434 func checkMigratedTable(t *testing.T, tableName, expectColumn string) { 435 for i := range clusterInstance.Keyspaces[0].Shards { 436 createStatement := getCreateTableStatement(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], tableName) 437 assert.Contains(t, createStatement, expectColumn) 438 } 439 } 440 441 // getCreateTableStatement returns the CREATE TABLE statement for a given table 442 func getCreateTableStatement(t *testing.T, tablet *cluster.Vttablet, tableName string) (statement string) { 443 queryResult, err := tablet.VttabletProcess.QueryTablet(fmt.Sprintf("show create table %s;", tableName), keyspaceName, true) 444 require.Nil(t, err) 445 446 assert.Equal(t, len(queryResult.Rows), 1) 447 assert.Equal(t, len(queryResult.Rows[0]), 2) // table name, create statement 448 statement = queryResult.Rows[0][1].ToString() 449 return statement 450 }