vitess.io/vitess@v0.16.2/go/test/endtoend/onlineddl/vtgate_util.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 onlineddl 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "math" 24 "net/http" 25 "os" 26 "testing" 27 "time" 28 29 "vitess.io/vitess/go/mysql" 30 "vitess.io/vitess/go/sqltypes" 31 "vitess.io/vitess/go/vt/log" 32 "vitess.io/vitess/go/vt/schema" 33 "vitess.io/vitess/go/vt/sqlparser" 34 35 "vitess.io/vitess/go/test/endtoend/cluster" 36 37 "github.com/buger/jsonparser" 38 "github.com/stretchr/testify/assert" 39 "github.com/stretchr/testify/require" 40 ) 41 42 // VtgateExecQuery runs a query on VTGate using given query params 43 func VtgateExecQuery(t *testing.T, vtParams *mysql.ConnParams, query string, expectError string) *sqltypes.Result { 44 t.Helper() 45 46 ctx := context.Background() 47 conn, err := mysql.Connect(ctx, vtParams) 48 require.Nil(t, err) 49 defer conn.Close() 50 51 qr, err := conn.ExecuteFetch(query, math.MaxInt64, true) 52 if expectError == "" { 53 require.NoError(t, err) 54 } else { 55 require.Error(t, err, "error should not be nil") 56 assert.Contains(t, err.Error(), expectError, "Unexpected error") 57 } 58 return qr 59 } 60 61 // VtgateExecDDL executes a DDL query with given strategy 62 func VtgateExecDDL(t *testing.T, vtParams *mysql.ConnParams, ddlStrategy string, query string, expectError string) *sqltypes.Result { 63 t.Helper() 64 65 ctx := context.Background() 66 conn, err := mysql.Connect(ctx, vtParams) 67 require.Nil(t, err) 68 defer conn.Close() 69 70 setSession := fmt.Sprintf("set @@ddl_strategy='%s'", ddlStrategy) 71 _, err = conn.ExecuteFetch(setSession, 1000, true) 72 assert.NoError(t, err) 73 74 qr, err := conn.ExecuteFetch(query, 1000, true) 75 if expectError == "" { 76 require.NoError(t, err) 77 } else { 78 require.Error(t, err, "error should not be nil") 79 assert.Contains(t, err.Error(), expectError, "Unexpected error") 80 } 81 return qr 82 } 83 84 // CheckRetryMigration attempts to retry a migration, and expects success/failure by counting affected rows 85 func CheckRetryMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectRetryPossible bool) { 86 query, err := sqlparser.ParseAndBind("alter vitess_migration %a retry", 87 sqltypes.StringBindVariable(uuid), 88 ) 89 require.NoError(t, err) 90 r := VtgateExecQuery(t, vtParams, query, "") 91 92 if expectRetryPossible { 93 assert.Equal(t, len(shards), int(r.RowsAffected)) 94 } else { 95 assert.Equal(t, int(0), int(r.RowsAffected)) 96 } 97 } 98 99 // CheckRetryPartialMigration attempts to retry a migration where a subset of shards failed 100 func CheckRetryPartialMigration(t *testing.T, vtParams *mysql.ConnParams, uuid string, expectAtLeastRowsAffected uint64) { 101 query, err := sqlparser.ParseAndBind("alter vitess_migration %a retry", 102 sqltypes.StringBindVariable(uuid), 103 ) 104 require.NoError(t, err) 105 r := VtgateExecQuery(t, vtParams, query, "") 106 107 assert.GreaterOrEqual(t, expectAtLeastRowsAffected, r.RowsAffected) 108 } 109 110 // CheckCancelMigration attempts to cancel a migration, and expects success/failure by counting affected rows 111 func CheckCancelMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectCancelPossible bool) { 112 query, err := sqlparser.ParseAndBind("alter vitess_migration %a cancel", 113 sqltypes.StringBindVariable(uuid), 114 ) 115 require.NoError(t, err) 116 r := VtgateExecQuery(t, vtParams, query, "") 117 118 if expectCancelPossible { 119 assert.Equal(t, len(shards), int(r.RowsAffected)) 120 } else { 121 assert.Equal(t, int(0), int(r.RowsAffected)) 122 } 123 } 124 125 // CheckCleanupMigration attempts to cleanup a migration, and expects success by counting affected rows 126 func CheckCleanupMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string) { 127 query, err := sqlparser.ParseAndBind("alter vitess_migration %a cleanup", 128 sqltypes.StringBindVariable(uuid), 129 ) 130 require.NoError(t, err) 131 r := VtgateExecQuery(t, vtParams, query, "") 132 133 assert.Equal(t, len(shards), int(r.RowsAffected)) 134 } 135 136 // CheckCompleteMigration attempts to complete a migration, and expects success by counting affected rows 137 func CheckCompleteMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectCompletePossible bool) { 138 query, err := sqlparser.ParseAndBind("alter vitess_migration %a complete", 139 sqltypes.StringBindVariable(uuid), 140 ) 141 require.NoError(t, err) 142 r := VtgateExecQuery(t, vtParams, query, "") 143 144 if expectCompletePossible { 145 assert.Equal(t, len(shards), int(r.RowsAffected)) 146 } else { 147 assert.Equal(t, int(0), int(r.RowsAffected)) 148 } 149 } 150 151 // CheckLaunchMigration attempts to launch a migration, and expects success by counting affected rows 152 func CheckLaunchMigration(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, launchShards string, expectLaunchPossible bool) { 153 query, err := sqlparser.ParseAndBind("alter vitess_migration %a launch vitess_shards %a", 154 sqltypes.StringBindVariable(uuid), 155 sqltypes.StringBindVariable(launchShards), 156 ) 157 require.NoError(t, err) 158 r := VtgateExecQuery(t, vtParams, query, "") 159 160 if expectLaunchPossible { 161 assert.Equal(t, len(shards), int(r.RowsAffected)) 162 } else { 163 assert.Equal(t, int(0), int(r.RowsAffected)) 164 } 165 } 166 167 // CheckCompleteAllMigrations completes all pending migrations and expect number of affected rows 168 // A negative value for expectCount indicates "don't care, no need to check" 169 func CheckCompleteAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) { 170 completeQuery := "alter vitess_migration complete all" 171 r := VtgateExecQuery(t, vtParams, completeQuery, "") 172 173 if expectCount >= 0 { 174 assert.Equal(t, expectCount, int(r.RowsAffected)) 175 } 176 } 177 178 // CheckCancelAllMigrations cancels all pending migrations and expect number of affected rows 179 // A negative value for expectCount indicates "don't care, no need to check" 180 func CheckCancelAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) { 181 cancelQuery := "alter vitess_migration cancel all" 182 r := VtgateExecQuery(t, vtParams, cancelQuery, "") 183 184 if expectCount >= 0 { 185 assert.Equal(t, expectCount, int(r.RowsAffected)) 186 } 187 } 188 189 // CheckLaunchAllMigrations launches all queued posponed migrations and expect number of affected rows 190 // A negative value for expectCount indicates "don't care, no need to check" 191 func CheckLaunchAllMigrations(t *testing.T, vtParams *mysql.ConnParams, expectCount int) { 192 completeQuery := "alter vitess_migration launch all" 193 r := VtgateExecQuery(t, vtParams, completeQuery, "") 194 195 if expectCount >= 0 { 196 assert.Equal(t, expectCount, int(r.RowsAffected)) 197 } 198 } 199 200 // CheckMigrationStatus verifies that the migration indicated by given UUID has the given expected status 201 func CheckMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectStatuses ...schema.OnlineDDLStatus) { 202 query, err := sqlparser.ParseAndBind("show vitess_migrations like %a", 203 sqltypes.StringBindVariable(uuid), 204 ) 205 require.NoError(t, err) 206 207 r := VtgateExecQuery(t, vtParams, query, "") 208 fmt.Printf("# output for `%s`:\n", query) 209 PrintQueryResult(os.Stdout, r) 210 211 count := 0 212 for _, row := range r.Named().Rows { 213 if row["migration_uuid"].ToString() != uuid { 214 continue 215 } 216 for _, expectStatus := range expectStatuses { 217 if row["migration_status"].ToString() == string(expectStatus) { 218 count++ 219 break 220 } 221 } 222 } 223 assert.Equal(t, len(shards), count) 224 } 225 226 // WaitForMigrationStatus waits for a migration to reach either provided statuses (returns immediately), or eventually time out 227 func WaitForMigrationStatus(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, timeout time.Duration, expectStatuses ...schema.OnlineDDLStatus) schema.OnlineDDLStatus { 228 shardNames := map[string]bool{} 229 for _, shard := range shards { 230 shardNames[shard.Name] = true 231 } 232 query, err := sqlparser.ParseAndBind("show vitess_migrations like %a", 233 sqltypes.StringBindVariable(uuid), 234 ) 235 require.NoError(t, err) 236 237 statusesMap := map[string]bool{} 238 for _, status := range expectStatuses { 239 statusesMap[string(status)] = true 240 } 241 startTime := time.Now() 242 lastKnownStatus := "" 243 for time.Since(startTime) < timeout { 244 countMatchedShards := 0 245 r := VtgateExecQuery(t, vtParams, query, "") 246 for _, row := range r.Named().Rows { 247 shardName := row["shard"].ToString() 248 if !shardNames[shardName] { 249 // irrelevant shard 250 continue 251 } 252 lastKnownStatus = row["migration_status"].ToString() 253 if row["migration_uuid"].ToString() == uuid && statusesMap[lastKnownStatus] { 254 countMatchedShards++ 255 } 256 } 257 if countMatchedShards == len(shards) { 258 return schema.OnlineDDLStatus(lastKnownStatus) 259 } 260 time.Sleep(1 * time.Second) 261 } 262 return schema.OnlineDDLStatus(lastKnownStatus) 263 } 264 265 // CheckMigrationArtifacts verifies given migration exists, and checks if it has artifacts 266 func CheckMigrationArtifacts(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard, uuid string, expectArtifacts bool) { 267 r := ReadMigrations(t, vtParams, uuid) 268 269 assert.Equal(t, len(shards), len(r.Named().Rows)) 270 for _, row := range r.Named().Rows { 271 hasArtifacts := (row["artifacts"].ToString() != "") 272 assert.Equal(t, expectArtifacts, hasArtifacts) 273 } 274 } 275 276 // ReadMigrations reads migration entries 277 func ReadMigrations(t *testing.T, vtParams *mysql.ConnParams, like string) *sqltypes.Result { 278 query, err := sqlparser.ParseAndBind("show vitess_migrations like %a", 279 sqltypes.StringBindVariable(like), 280 ) 281 require.NoError(t, err) 282 283 return VtgateExecQuery(t, vtParams, query, "") 284 } 285 286 // ReadMigrationLogs reads migration logs for a given migration, on all shards 287 func ReadMigrationLogs(t *testing.T, vtParams *mysql.ConnParams, uuid string) (logs []string) { 288 query, err := sqlparser.ParseAndBind("show vitess_migration %a logs", 289 sqltypes.StringBindVariable(uuid), 290 ) 291 require.NoError(t, err) 292 293 r := VtgateExecQuery(t, vtParams, query, "") 294 for _, row := range r.Named().Rows { 295 migrationLog := row["migration_log"].ToString() 296 logs = append(logs, migrationLog) 297 } 298 return logs 299 } 300 301 // ThrottleAllMigrations fully throttles online-ddl apps 302 func ThrottleAllMigrations(t *testing.T, vtParams *mysql.ConnParams) { 303 query := "alter vitess_migration throttle all expire '24h' ratio 1" 304 _ = VtgateExecQuery(t, vtParams, query, "") 305 } 306 307 // UnthrottleAllMigrations cancels migration throttling 308 func UnthrottleAllMigrations(t *testing.T, vtParams *mysql.ConnParams) { 309 query := "alter vitess_migration unthrottle all" 310 _ = VtgateExecQuery(t, vtParams, query, "") 311 } 312 313 // CheckThrottledApps checks for existence or non-existence of an app in the throttled apps list 314 func CheckThrottledApps(t *testing.T, vtParams *mysql.ConnParams, appName string, expectFind bool) { 315 query := "show vitess_throttled_apps" 316 r := VtgateExecQuery(t, vtParams, query, "") 317 318 found := false 319 for _, row := range r.Named().Rows { 320 if row.AsString("app", "") == appName { 321 found = true 322 } 323 } 324 assert.Equal(t, expectFind, found, "check app %v in throttled apps: %v", appName, found) 325 } 326 327 // WaitForThrottledTimestamp waits for a migration to have a non-empty last_throttled_timestamp 328 func WaitForThrottledTimestamp(t *testing.T, vtParams *mysql.ConnParams, uuid string, timeout time.Duration) ( 329 row sqltypes.RowNamedValues, 330 startedTimestamp string, 331 lastThrottledTimestamp string, 332 ) { 333 startTime := time.Now() 334 for time.Since(startTime) < timeout { 335 rs := ReadMigrations(t, vtParams, uuid) 336 require.NotNil(t, rs) 337 for _, row = range rs.Named().Rows { 338 startedTimestamp = row.AsString("started_timestamp", "") 339 require.NotEmpty(t, startedTimestamp) 340 lastThrottledTimestamp = row.AsString("last_throttled_timestamp", "") 341 if lastThrottledTimestamp != "" { 342 // good. This is what we've been waiting for. 343 return row, startedTimestamp, lastThrottledTimestamp 344 } 345 } 346 time.Sleep(1 * time.Second) 347 } 348 t.Error("timeout waiting for last_throttled_timestamp to have nonempty value") 349 return 350 } 351 352 // WaitForThrottlerStatusEnabled waits for a tablet to report its throttler status as enabled. 353 func WaitForThrottlerStatusEnabled(t *testing.T, tablet *cluster.Vttablet, timeout time.Duration) { 354 jsonPath := "IsEnabled" 355 url := fmt.Sprintf("http://localhost:%d/throttler/status", tablet.HTTPPort) 356 357 ctx, cancel := context.WithTimeout(context.Background(), throttlerConfigTimeout) 358 defer cancel() 359 360 ticker := time.NewTicker(time.Second) 361 defer ticker.Stop() 362 363 for { 364 body := getHTTPBody(url) 365 val, err := jsonparser.GetBoolean([]byte(body), jsonPath) 366 require.NoError(t, err) 367 if val { 368 return 369 } 370 select { 371 case <-ctx.Done(): 372 t.Error("timeout waiting for tablet's throttler status to be enabled") 373 return 374 case <-ticker.C: 375 } 376 } 377 } 378 379 func getHTTPBody(url string) string { 380 resp, err := http.Get(url) 381 if err != nil { 382 log.Infof("http Get returns %+v", err) 383 return "" 384 } 385 if resp.StatusCode != 200 { 386 log.Infof("http Get returns status %d", resp.StatusCode) 387 return "" 388 } 389 respByte, _ := io.ReadAll(resp.Body) 390 defer resp.Body.Close() 391 body := string(respByte) 392 return body 393 } 394 395 // ValidateSequentialMigrationIDs validates that schem_migrations.id column, which is an AUTO_INCREMENT, does 396 // not have gaps 397 func ValidateSequentialMigrationIDs(t *testing.T, vtParams *mysql.ConnParams, shards []cluster.Shard) { 398 r := VtgateExecQuery(t, vtParams, "show vitess_migrations", "") 399 shardMin := map[string]uint64{} 400 shardMax := map[string]uint64{} 401 shardCount := map[string]uint64{} 402 403 for _, row := range r.Named().Rows { 404 id := row.AsUint64("id", 0) 405 require.NotZero(t, id) 406 407 shard := row.AsString("shard", "") 408 require.NotEmpty(t, shard) 409 410 if _, ok := shardMin[shard]; !ok { 411 shardMin[shard] = id 412 shardMax[shard] = id 413 } 414 if id < shardMin[shard] { 415 shardMin[shard] = id 416 } 417 if id > shardMax[shard] { 418 shardMax[shard] = id 419 } 420 shardCount[shard]++ 421 } 422 require.NotEmpty(t, shards) 423 assert.Equal(t, len(shards), len(shardMin)) 424 assert.Equal(t, len(shards), len(shardMax)) 425 assert.Equal(t, len(shards), len(shardCount)) 426 for shard, count := range shardCount { 427 assert.NotZero(t, count) 428 assert.Equalf(t, count, shardMax[shard]-shardMin[shard]+1, "mismatch: shared=%v, count=%v, min=%v, max=%v", shard, count, shardMin[shard], shardMax[shard]) 429 } 430 }