vitess.io/vitess@v0.16.2/go/vt/wrangler/vexec_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 17 package wrangler 18 19 import ( 20 "context" 21 "fmt" 22 "regexp" 23 "sort" 24 "strings" 25 "testing" 26 "time" 27 28 "github.com/stretchr/testify/require" 29 30 "vitess.io/vitess/go/sqltypes" 31 "vitess.io/vitess/go/vt/logutil" 32 ) 33 34 func TestVExec(t *testing.T) { 35 ctx := context.Background() 36 workflow := "wrWorkflow" 37 keyspace := "target" 38 query := "update _vt.vreplication set state = 'Running'" 39 env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, time.Now().Unix()) 40 defer env.close() 41 var logger = logutil.NewMemoryLogger() 42 wr := New(logger, env.topoServ, env.tmc) 43 44 vx := newVExec(ctx, workflow, keyspace, query, wr) 45 err := vx.getPrimaries() 46 require.Nil(t, err) 47 primaries := vx.primaries 48 require.NotNil(t, primaries) 49 require.Equal(t, len(primaries), 2) 50 var shards []string 51 for _, primary := range primaries { 52 shards = append(shards, primary.Shard) 53 } 54 sort.Strings(shards) 55 require.Equal(t, fmt.Sprintf("%v", shards), "[-80 80-]") 56 57 plan, err := vx.parseAndPlan(ctx) 58 require.NoError(t, err) 59 require.NotNil(t, plan) 60 61 addWheres := func(query string) string { 62 if strings.Contains(query, " where ") { 63 query += " and " 64 } else { 65 query += " where " 66 } 67 query += fmt.Sprintf("db_name = %s and workflow = %s", encodeString("vt_"+keyspace), encodeString(workflow)) 68 return query 69 } 70 want := addWheres(query) 71 require.Equal(t, want, plan.parsedQuery.Query) 72 73 vx.plannedQuery = plan.parsedQuery.Query 74 vx.exec() 75 76 res, err := wr.getStreams(ctx, workflow, keyspace) 77 require.NoError(t, err) 78 require.Less(t, res.MaxVReplicationLag, int64(3 /*seconds*/)) // lag should be very small 79 80 type TestCase struct { 81 name string 82 query string 83 result *sqltypes.Result 84 errorString string 85 } 86 87 var result *sqltypes.Result 88 var testCases []*TestCase 89 result = sqltypes.MakeTestResult(sqltypes.MakeTestFields( 90 "id|source|message|cell|tablet_types|workflow_type|workflow_sub_type|defer_secondary_keys", 91 "int64|varchar|varchar|varchar|varchar|int64|int64|int64"), 92 "1|keyspace:\"source\" shard:\"0\" filter:{rules:{match:\"t1\"}}||||0|0|0", 93 ) 94 testCases = append(testCases, &TestCase{ 95 name: "select", 96 query: "select id, source, message, cell, tablet_types, workflow_type, workflow_sub_type, defer_secondary_keys from _vt.vreplication", 97 result: result, 98 }) 99 result = &sqltypes.Result{ 100 RowsAffected: 1, 101 Rows: [][]sqltypes.Value{}, 102 } 103 testCases = append(testCases, &TestCase{ 104 name: "delete", 105 query: "delete from _vt.vreplication where message != ''", 106 result: result, 107 }) 108 result = &sqltypes.Result{ 109 RowsAffected: 1, 110 Rows: [][]sqltypes.Value{}, 111 } 112 testCases = append(testCases, &TestCase{ 113 name: "update", 114 query: "update _vt.vreplication set state='Stopped', message='for wrangler test'", 115 result: result, 116 }) 117 118 errorString := "query not supported by vexec" 119 testCases = append(testCases, &TestCase{ 120 name: "insert", 121 query: "insert into _vt.vreplication(state, workflow, db_name) values ('Running', 'wk1', 'ks1'), ('Stopped', 'wk1', 'ks1')", 122 errorString: errorString, 123 }) 124 125 errorString = "table not supported by vexec" 126 testCases = append(testCases, &TestCase{ 127 name: "delete invalid-other-table", 128 query: "delete from _vt.copy_state", 129 errorString: errorString, 130 }) 131 132 for _, testCase := range testCases { 133 t.Run(testCase.query, func(t *testing.T) { 134 results, err := wr.VExec(ctx, workflow, keyspace, testCase.query, false) 135 if testCase.errorString == "" { 136 require.NoError(t, err) 137 for _, result := range results { 138 if !testCase.result.Equal(result) { 139 t.Errorf("mismatched result:\nwant: %v\ngot: %v", testCase.result, result) 140 } 141 } 142 } else { 143 require.Error(t, err) 144 if !strings.Contains(err.Error(), testCase.errorString) { 145 t.Fatalf("Wrong error, want %s, got %s", testCase.errorString, err.Error()) 146 } 147 } 148 }) 149 } 150 151 query = "delete from _vt.vreplication" 152 _, err = wr.VExec(ctx, workflow, keyspace, query, true) 153 require.NoError(t, err) 154 dryRunResults := []string{ 155 "Query: delete from _vt.vreplication where db_name = 'vt_target' and workflow = 'wrWorkflow'", 156 "will be run on the following streams in keyspace target for workflow wrWorkflow:\n\n", 157 `+----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 158 | TABLET | ID | BINLOGSOURCE | STATE | DBNAME | CURRENT GTID | 159 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 160 | -80/zone1-0000000200 | 1 | keyspace:"source" shard:"0" | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 | 161 | | | filter:{rules:{match:"t1"}} | | | | 162 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 163 | 80-/zone1-0000000210 | 1 | keyspace:"source" shard:"0" | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 | 164 | | | filter:{rules:{match:"t1"}} | | | | 165 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+`, 166 } 167 require.Equal(t, strings.Join(dryRunResults, "\n")+"\n\n\n\n\n", logger.String()) 168 logger.Clear() 169 } 170 171 func TestWorkflowStatusUpdate(t *testing.T) { 172 require.Equal(t, "Running", updateState("for vdiff", "Running", nil, int64(time.Now().Second()))) 173 require.Equal(t, "Running", updateState("", "Running", nil, int64(time.Now().Second()))) 174 require.Equal(t, "Lagging", updateState("", "Running", nil, int64(time.Now().Second())-100)) 175 require.Equal(t, "Copying", updateState("", "Running", []copyState{{Table: "t1", LastPK: "[[INT64(10)]]"}}, int64(time.Now().Second()))) 176 require.Equal(t, "Error", updateState("error: primary tablet not contactable", "Running", nil, 0)) 177 } 178 179 func TestWorkflowListStreams(t *testing.T) { 180 ctx := context.Background() 181 workflow := "wrWorkflow" 182 keyspace := "target" 183 env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 1234) 184 defer env.close() 185 logger := logutil.NewMemoryLogger() 186 wr := New(logger, env.topoServ, env.tmc) 187 188 _, err := wr.WorkflowAction(ctx, workflow, keyspace, "listall", false) 189 require.NoError(t, err) 190 191 _, err = wr.WorkflowAction(ctx, workflow, "badks", "show", false) 192 require.Errorf(t, err, "node doesn't exist: keyspaces/badks/shards") 193 194 _, err = wr.WorkflowAction(ctx, "badwf", keyspace, "show", false) 195 require.Errorf(t, err, "no streams found for workflow badwf in keyspace target") 196 logger.Clear() 197 _, err = wr.WorkflowAction(ctx, workflow, keyspace, "show", false) 198 require.NoError(t, err) 199 want := `{ 200 "Workflow": "wrWorkflow", 201 "SourceLocation": { 202 "Keyspace": "source", 203 "Shards": [ 204 "0" 205 ] 206 }, 207 "TargetLocation": { 208 "Keyspace": "target", 209 "Shards": [ 210 "-80", 211 "80-" 212 ] 213 }, 214 "MaxVReplicationLag": 0, 215 "MaxVReplicationTransactionLag": 0, 216 "Frozen": false, 217 "ShardStatuses": { 218 "-80/zone1-0000000200": { 219 "PrimaryReplicationStatuses": [ 220 { 221 "Shard": "-80", 222 "Tablet": "zone1-0000000200", 223 "ID": 1, 224 "Bls": { 225 "keyspace": "source", 226 "shard": "0", 227 "filter": { 228 "rules": [ 229 { 230 "match": "t1" 231 } 232 ] 233 } 234 }, 235 "Pos": "14b68925-696a-11ea-aee7-fec597a91f5e:1-3", 236 "StopPos": "", 237 "State": "Copying", 238 "DBName": "vt_target", 239 "TransactionTimestamp": 0, 240 "TimeUpdated": 1234, 241 "TimeHeartbeat": 1234, 242 "TimeThrottled": 0, 243 "ComponentThrottled": "", 244 "Message": "", 245 "Tags": "", 246 "WorkflowType": "Materialize", 247 "WorkflowSubType": "None", 248 "CopyState": [ 249 { 250 "Table": "t1", 251 "LastPK": "pk1" 252 } 253 ] 254 } 255 ], 256 "TabletControls": null, 257 "PrimaryIsServing": true 258 }, 259 "80-/zone1-0000000210": { 260 "PrimaryReplicationStatuses": [ 261 { 262 "Shard": "80-", 263 "Tablet": "zone1-0000000210", 264 "ID": 1, 265 "Bls": { 266 "keyspace": "source", 267 "shard": "0", 268 "filter": { 269 "rules": [ 270 { 271 "match": "t1" 272 } 273 ] 274 } 275 }, 276 "Pos": "14b68925-696a-11ea-aee7-fec597a91f5e:1-3", 277 "StopPos": "", 278 "State": "Copying", 279 "DBName": "vt_target", 280 "TransactionTimestamp": 0, 281 "TimeUpdated": 1234, 282 "TimeHeartbeat": 1234, 283 "TimeThrottled": 0, 284 "ComponentThrottled": "", 285 "Message": "", 286 "Tags": "", 287 "WorkflowType": "Materialize", 288 "WorkflowSubType": "None", 289 "CopyState": [ 290 { 291 "Table": "t1", 292 "LastPK": "pk1" 293 } 294 ] 295 } 296 ], 297 "TabletControls": null, 298 "PrimaryIsServing": true 299 } 300 }, 301 "SourceTimeZone": "", 302 "TargetTimeZone": "" 303 } 304 305 ` 306 got := logger.String() 307 // MaxVReplicationLag needs to be reset. This can't be determinable in this kind of a test because time.Now() is constantly shifting. 308 re := regexp.MustCompile(`"MaxVReplicationLag": \d+`) 309 got = re.ReplaceAllLiteralString(got, `"MaxVReplicationLag": 0`) 310 re = regexp.MustCompile(`"MaxVReplicationTransactionLag": \d+`) 311 got = re.ReplaceAllLiteralString(got, `"MaxVReplicationTransactionLag": 0`) 312 require.Equal(t, want, got) 313 314 results, err := wr.execWorkflowAction(ctx, workflow, keyspace, "stop", false) 315 require.Nil(t, err) 316 317 // convert map to list and sort it for comparison 318 var gotResults []string 319 for key, result := range results { 320 gotResults = append(gotResults, fmt.Sprintf("%s:%v", key.String(), result)) 321 } 322 sort.Strings(gotResults) 323 wantResults := []string{"Tablet{zone1-0000000200}:rows_affected:1", "Tablet{zone1-0000000210}:rows_affected:1"} 324 sort.Strings(wantResults) 325 require.ElementsMatch(t, wantResults, gotResults) 326 327 logger.Clear() 328 results, err = wr.execWorkflowAction(ctx, workflow, keyspace, "stop", true) 329 require.Nil(t, err) 330 require.Equal(t, "map[]", fmt.Sprintf("%v", results)) 331 dryRunResult := `Query: update _vt.vreplication set state = 'Stopped' where db_name = 'vt_target' and workflow = 'wrWorkflow' 332 will be run on the following streams in keyspace target for workflow wrWorkflow: 333 334 335 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 336 | TABLET | ID | BINLOGSOURCE | STATE | DBNAME | CURRENT GTID | 337 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 338 | -80/zone1-0000000200 | 1 | keyspace:"source" shard:"0" | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 | 339 | | | filter:{rules:{match:"t1"}} | | | | 340 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 341 | 80-/zone1-0000000210 | 1 | keyspace:"source" shard:"0" | Copying | vt_target | 14b68925-696a-11ea-aee7-fec597a91f5e:1-3 | 342 | | | filter:{rules:{match:"t1"}} | | | | 343 +----------------------+----+--------------------------------+---------+-----------+------------------------------------------+ 344 345 346 347 348 ` 349 require.Equal(t, dryRunResult, logger.String()) 350 } 351 352 func TestWorkflowListAll(t *testing.T) { 353 ctx := context.Background() 354 keyspace := "target" 355 workflow := "wrWorkflow" 356 env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 0) 357 defer env.close() 358 logger := logutil.NewMemoryLogger() 359 wr := New(logger, env.topoServ, env.tmc) 360 361 workflows, err := wr.ListAllWorkflows(ctx, keyspace, true) 362 require.Nil(t, err) 363 require.Equal(t, []string{workflow}, workflows) 364 365 workflows, err = wr.ListAllWorkflows(ctx, keyspace, false) 366 require.Nil(t, err) 367 require.Equal(t, []string{workflow, "wrWorkflow2"}, workflows) 368 logger.Clear() 369 } 370 371 func TestVExecValidations(t *testing.T) { 372 ctx := context.Background() 373 workflow := "wf" 374 keyspace := "ks" 375 query := "" 376 env := newWranglerTestEnv(t, []string{"0"}, []string{"-80", "80-"}, "", nil, 0) 377 defer env.close() 378 379 wr := New(logutil.NewConsoleLogger(), env.topoServ, env.tmc) 380 381 vx := newVExec(ctx, workflow, keyspace, query, wr) 382 383 type badQuery struct { 384 name string 385 query string 386 errorString string 387 } 388 badQueries := []badQuery{ 389 { 390 name: "invalid", 391 query: "bad query", 392 errorString: "syntax error at position 4 near 'bad'", 393 }, 394 { 395 name: "incorrect table", 396 query: "select * from _vt.vreplication2", 397 errorString: "table not supported by vexec: _vt.vreplication2", 398 }, 399 { 400 name: "unsupported query", 401 query: "describe _vt.vreplication", 402 errorString: "query not supported by vexec: explain _vt.vreplication", 403 }, 404 } 405 for _, bq := range badQueries { 406 t.Run(bq.name, func(t *testing.T) { 407 vx.query = bq.query 408 plan, err := vx.parseAndPlan(ctx) 409 require.EqualError(t, err, bq.errorString) 410 require.Nil(t, plan) 411 }) 412 } 413 414 type action struct { 415 name string 416 want string 417 expectedError error 418 } 419 updateSQL := "update _vt.vreplication set state = %s" 420 actions := []action{ 421 { 422 name: "start", 423 want: fmt.Sprintf(updateSQL, encodeString("Running")), 424 expectedError: nil, 425 }, 426 { 427 name: "stop", 428 want: fmt.Sprintf(updateSQL, encodeString("Stopped")), 429 expectedError: nil, 430 }, 431 { 432 name: "delete", 433 want: "delete from _vt.vreplication", 434 expectedError: nil, 435 }, 436 { 437 name: "other", 438 want: "", 439 expectedError: fmt.Errorf("invalid action found: other"), 440 }} 441 442 for _, a := range actions { 443 t.Run(a.name, func(t *testing.T) { 444 sql, err := wr.getWorkflowActionQuery(a.name) 445 require.Equal(t, a.expectedError, err) 446 require.Equal(t, a.want, sql) 447 }) 448 } 449 }