vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletmanager/vreplication/framework_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 vreplication 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "os" 24 "reflect" 25 "regexp" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/spf13/pflag" 31 "github.com/stretchr/testify/require" 32 "google.golang.org/protobuf/proto" 33 34 _flag "vitess.io/vitess/go/internal/flag" 35 "vitess.io/vitess/go/mysql" 36 "vitess.io/vitess/go/sqltypes" 37 "vitess.io/vitess/go/vt/binlog/binlogplayer" 38 "vitess.io/vitess/go/vt/dbconfigs" 39 "vitess.io/vitess/go/vt/grpcclient" 40 "vitess.io/vitess/go/vt/log" 41 "vitess.io/vitess/go/vt/servenv" 42 "vitess.io/vitess/go/vt/sidecardb" 43 "vitess.io/vitess/go/vt/topo" 44 "vitess.io/vitess/go/vt/vttablet/queryservice" 45 "vitess.io/vitess/go/vt/vttablet/queryservice/fakes" 46 "vitess.io/vitess/go/vt/vttablet/tabletconn" 47 "vitess.io/vitess/go/vt/vttablet/tabletconntest" 48 qh "vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication/queryhistory" 49 "vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer" 50 "vitess.io/vitess/go/vt/vttablet/tabletserver/vstreamer/testenv" 51 52 binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata" 53 querypb "vitess.io/vitess/go/vt/proto/query" 54 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 55 ) 56 57 var ( 58 playerEngine *Engine 59 streamerEngine *vstreamer.Engine 60 env *testenv.Env 61 globalFBC = &fakeBinlogClient{} 62 vrepldb = "vrepl" 63 globalDBQueries = make(chan string, 1000) 64 testForeignKeyQueries = false 65 doNotLogDBQueries = false 66 ) 67 68 type LogExpectation struct { 69 Type string 70 Detail string 71 } 72 73 var heartbeatRe *regexp.Regexp 74 75 // setFlag() sets a flag for a test in a non-racy way: 76 // - it registers the flag using a different flagset scope 77 // - clears other flags by passing a dummy os.Args() while parsing this flagset 78 // - sets the specific flag, if it has not already been defined 79 // - resets the os.Args() so that the remaining flagsets can be parsed correctly 80 func setFlag(flagName, flagValue string) { 81 flagSetName := "vreplication-unit-test" 82 var tmp []string 83 tmp, os.Args = os.Args[:], []string{flagSetName} 84 defer func() { os.Args = tmp }() 85 86 servenv.OnParseFor(flagSetName, func(fs *pflag.FlagSet) { 87 if fs.Lookup(flagName) != nil { 88 fmt.Printf("found %s: %+v", flagName, fs.Lookup(flagName).Value) 89 return 90 } 91 }) 92 servenv.ParseFlags(flagSetName) 93 94 if err := pflag.Set(flagName, flagValue); err != nil { 95 msg := "failed to set flag %q to %q: %v" 96 log.Errorf(msg, flagName, flagValue, err) 97 } 98 } 99 100 func init() { 101 tabletconn.RegisterDialer("test", func(tablet *topodatapb.Tablet, failFast grpcclient.FailFast) (queryservice.QueryService, error) { 102 return &fakeTabletConn{ 103 QueryService: fakes.ErrorQueryService, 104 tablet: tablet, 105 }, nil 106 }) 107 tabletconntest.SetProtocol("go.vt.vttablet.tabletmanager.vreplication.framework_test", "test") 108 109 binlogplayer.RegisterClientFactory("test", func() binlogplayer.Client { return globalFBC }) 110 heartbeatRe = regexp.MustCompile(`update _vt.vreplication set time_updated=\d+ where id=\d+`) 111 } 112 113 func TestMain(m *testing.M) { 114 binlogplayer.SetProtocol("vreplication_test_framework", "test") 115 _flag.ParseFlagsForTest() 116 exitCode := func() int { 117 var err error 118 env, err = testenv.Init() 119 if err != nil { 120 fmt.Fprintf(os.Stderr, "%v", err) 121 return 1 122 } 123 defer env.Close() 124 125 vreplicationExperimentalFlags = 0 126 127 // engines cannot be initialized in testenv because it introduces 128 // circular dependencies. 129 streamerEngine = vstreamer.NewEngine(env.TabletEnv, env.SrvTopo, env.SchemaEngine, nil, env.Cells[0]) 130 streamerEngine.InitDBConfig(env.KeyspaceName, env.ShardName) 131 streamerEngine.Open() 132 defer streamerEngine.Close() 133 134 if err := env.Mysqld.ExecuteSuperQuery(context.Background(), fmt.Sprintf("create database %s", vrepldb)); err != nil { 135 fmt.Fprintf(os.Stderr, "%v", err) 136 return 1 137 } 138 139 if err := env.Mysqld.ExecuteSuperQuery(context.Background(), "set @@global.innodb_lock_wait_timeout=1"); err != nil { 140 fmt.Fprintf(os.Stderr, "%v", err) 141 return 1 142 } 143 144 externalConfig := map[string]*dbconfigs.DBConfigs{ 145 "exta": env.Dbcfgs, 146 "extb": env.Dbcfgs, 147 } 148 playerEngine = NewTestEngine(env.TopoServ, env.Cells[0], env.Mysqld, realDBClientFactory, realDBClientFactory, vrepldb, externalConfig) 149 playerEngine.Open(context.Background()) 150 defer playerEngine.Close() 151 152 return m.Run() 153 }() 154 os.Exit(exitCode) 155 } 156 157 func resetBinlogClient() { 158 globalFBC = &fakeBinlogClient{} 159 } 160 161 func primaryPosition(t *testing.T) string { 162 t.Helper() 163 pos, err := env.Mysqld.PrimaryPosition() 164 if err != nil { 165 t.Fatal(err) 166 } 167 return mysql.EncodePosition(pos) 168 } 169 170 func execStatements(t *testing.T, queries []string) { 171 t.Helper() 172 if err := env.Mysqld.ExecuteSuperQueryList(context.Background(), queries); err != nil { 173 log.Errorf("Error executing query: %s", err.Error()) 174 t.Error(err) 175 } 176 } 177 178 //-------------------------------------- 179 // Topos and tablets 180 181 func addTablet(id int) *topodatapb.Tablet { 182 tablet := &topodatapb.Tablet{ 183 Alias: &topodatapb.TabletAlias{ 184 Cell: env.Cells[0], 185 Uid: uint32(id), 186 }, 187 Keyspace: env.KeyspaceName, 188 Shard: env.ShardName, 189 KeyRange: &topodatapb.KeyRange{}, 190 Type: topodatapb.TabletType_REPLICA, 191 PortMap: map[string]int32{ 192 "test": int32(id), 193 }, 194 } 195 if err := env.TopoServ.CreateTablet(context.Background(), tablet); err != nil { 196 panic(err) 197 } 198 env.SchemaEngine.Reload(context.Background()) 199 return tablet 200 } 201 202 func addOtherTablet(id int, keyspace, shard string) *topodatapb.Tablet { 203 tablet := &topodatapb.Tablet{ 204 Alias: &topodatapb.TabletAlias{ 205 Cell: env.Cells[0], 206 Uid: uint32(id), 207 }, 208 Keyspace: keyspace, 209 Shard: shard, 210 KeyRange: &topodatapb.KeyRange{}, 211 Type: topodatapb.TabletType_REPLICA, 212 PortMap: map[string]int32{ 213 "test": int32(id), 214 }, 215 } 216 if err := env.TopoServ.CreateTablet(context.Background(), tablet); err != nil { 217 panic(err) 218 } 219 env.SchemaEngine.Reload(context.Background()) 220 return tablet 221 } 222 223 func deleteTablet(tablet *topodatapb.Tablet) { 224 env.TopoServ.DeleteTablet(context.Background(), tablet.Alias) 225 // This is not automatically removed from shard replication, which results in log spam. 226 topo.DeleteTabletReplicationData(context.Background(), env.TopoServ, tablet) 227 env.SchemaEngine.Reload(context.Background()) 228 } 229 230 // fakeTabletConn implement TabletConn interface. We only care about the 231 // health check part. The state reported by the tablet will depend 232 // on the Tag values "serving" and "healthy". 233 type fakeTabletConn struct { 234 queryservice.QueryService 235 tablet *topodatapb.Tablet 236 } 237 238 // StreamHealth is part of queryservice.QueryService. 239 func (ftc *fakeTabletConn) StreamHealth(ctx context.Context, callback func(*querypb.StreamHealthResponse) error) error { 240 return callback(&querypb.StreamHealthResponse{ 241 Serving: true, 242 Target: &querypb.Target{ 243 Keyspace: ftc.tablet.Keyspace, 244 Shard: ftc.tablet.Shard, 245 TabletType: ftc.tablet.Type, 246 }, 247 RealtimeStats: &querypb.RealtimeStats{}, 248 }) 249 } 250 251 // vstreamHook allows you to do work just before calling VStream. 252 var vstreamHook func(ctx context.Context) 253 254 // VStream directly calls into the pre-initialized engine. 255 func (ftc *fakeTabletConn) VStream(ctx context.Context, request *binlogdatapb.VStreamRequest, send func([]*binlogdatapb.VEvent) error) error { 256 if request.Target.Keyspace != "vttest" { 257 <-ctx.Done() 258 return io.EOF 259 } 260 if vstreamHook != nil { 261 vstreamHook(ctx) 262 } 263 return streamerEngine.Stream(ctx, request.Position, request.TableLastPKs, request.Filter, send) 264 } 265 266 // vstreamRowsHook allows you to do work just before calling VStreamRows. 267 var vstreamRowsHook func(ctx context.Context) 268 269 // vstreamRowsSendHook allows you to do work just before VStreamRows calls send. 270 var vstreamRowsSendHook func(ctx context.Context) 271 272 // VStreamRows directly calls into the pre-initialized engine. 273 func (ftc *fakeTabletConn) VStreamRows(ctx context.Context, request *binlogdatapb.VStreamRowsRequest, send func(*binlogdatapb.VStreamRowsResponse) error) error { 274 if vstreamRowsHook != nil { 275 vstreamRowsHook(ctx) 276 } 277 var row []sqltypes.Value 278 if request.Lastpk != nil { 279 r := sqltypes.Proto3ToResult(request.Lastpk) 280 if len(r.Rows) != 1 { 281 return fmt.Errorf("unexpected lastpk input: %v", request.Lastpk) 282 } 283 row = r.Rows[0] 284 } 285 return streamerEngine.StreamRows(ctx, request.Query, row, func(rows *binlogdatapb.VStreamRowsResponse) error { 286 if vstreamRowsSendHook != nil { 287 vstreamRowsSendHook(ctx) 288 } 289 return send(rows) 290 }) 291 } 292 293 //-------------------------------------- 294 // Binlog Client to TabletManager 295 296 // fakeBinlogClient satisfies binlogplayer.Client. 297 // Not to be used concurrently. 298 type fakeBinlogClient struct { 299 lastTablet *topodatapb.Tablet 300 lastPos string 301 lastTables []string 302 lastKeyRange *topodatapb.KeyRange 303 lastCharset *binlogdatapb.Charset 304 } 305 306 func (fbc *fakeBinlogClient) Dial(tablet *topodatapb.Tablet) error { 307 fbc.lastTablet = tablet 308 return nil 309 } 310 311 func (fbc *fakeBinlogClient) Close() { 312 } 313 314 func (fbc *fakeBinlogClient) StreamTables(ctx context.Context, position string, tables []string, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) { 315 fbc.lastPos = position 316 fbc.lastTables = tables 317 fbc.lastCharset = charset 318 return &btStream{ctx: ctx}, nil 319 } 320 321 func (fbc *fakeBinlogClient) StreamKeyRange(ctx context.Context, position string, keyRange *topodatapb.KeyRange, charset *binlogdatapb.Charset) (binlogplayer.BinlogTransactionStream, error) { 322 fbc.lastPos = position 323 fbc.lastKeyRange = keyRange 324 fbc.lastCharset = charset 325 return &btStream{ctx: ctx}, nil 326 } 327 328 // btStream satisfies binlogplayer.BinlogTransactionStream 329 type btStream struct { 330 ctx context.Context 331 sent bool 332 } 333 334 func (bts *btStream) Recv() (*binlogdatapb.BinlogTransaction, error) { 335 if !bts.sent { 336 bts.sent = true 337 return &binlogdatapb.BinlogTransaction{ 338 Statements: []*binlogdatapb.BinlogTransaction_Statement{ 339 { 340 Category: binlogdatapb.BinlogTransaction_Statement_BL_INSERT, 341 Sql: []byte("insert into t values(1)"), 342 }, 343 }, 344 EventToken: &querypb.EventToken{ 345 Timestamp: 72, 346 Position: "MariaDB/0-1-1235", 347 }, 348 }, nil 349 } 350 <-bts.ctx.Done() 351 return nil, bts.ctx.Err() 352 } 353 354 func expectFBCRequest(t *testing.T, tablet *topodatapb.Tablet, pos string, tables []string, kr *topodatapb.KeyRange) { 355 t.Helper() 356 if !proto.Equal(tablet, globalFBC.lastTablet) { 357 t.Errorf("Request tablet: %v, want %v", globalFBC.lastTablet, tablet) 358 } 359 if pos != globalFBC.lastPos { 360 t.Errorf("Request pos: %v, want %v", globalFBC.lastPos, pos) 361 } 362 if !reflect.DeepEqual(tables, globalFBC.lastTables) { 363 t.Errorf("Request tables: %v, want %v", globalFBC.lastTables, tables) 364 } 365 if !proto.Equal(kr, globalFBC.lastKeyRange) { 366 t.Errorf("Request KeyRange: %v, want %v", globalFBC.lastKeyRange, kr) 367 } 368 } 369 370 //-------------------------------------- 371 // DBCLient wrapper 372 373 func realDBClientFactory() binlogplayer.DBClient { 374 return &realDBClient{} 375 } 376 377 type realDBClient struct { 378 conn *mysql.Conn 379 nolog bool 380 } 381 382 func (dbc *realDBClient) DBName() string { 383 return vrepldb 384 } 385 386 func (dbc *realDBClient) Connect() error { 387 app, err := env.Dbcfgs.AppWithDB().MysqlParams() 388 if err != nil { 389 return err 390 } 391 app.DbName = vrepldb 392 conn, err := mysql.Connect(context.Background(), app) 393 if err != nil { 394 return err 395 } 396 dbc.conn = conn 397 return nil 398 } 399 400 func (dbc *realDBClient) Begin() error { 401 _, err := dbc.ExecuteFetch("begin", 10000) 402 return err 403 } 404 405 func (dbc *realDBClient) Commit() error { 406 _, err := dbc.ExecuteFetch("commit", 10000) 407 return err 408 } 409 410 func (dbc *realDBClient) Rollback() error { 411 _, err := dbc.ExecuteFetch("rollback", 10000) 412 return err 413 } 414 415 func (dbc *realDBClient) Close() { 416 dbc.conn.Close() 417 } 418 419 func (dbc *realDBClient) ExecuteFetch(query string, maxrows int) (*sqltypes.Result, error) { 420 // Use Clone() because the contents of memory region referenced by 421 // string can change when clients (e.g. vcopier) use unsafe string methods. 422 query = strings.Clone(query) 423 qr, err := dbc.conn.ExecuteFetch(query, 10000, true) 424 if doNotLogDBQueries { 425 return qr, err 426 } 427 if !strings.HasPrefix(query, "select") && !strings.HasPrefix(query, "set") && !dbc.nolog { 428 globalDBQueries <- query 429 } else if testForeignKeyQueries && strings.Contains(query, "foreign_key_checks") { //allow select/set for foreign_key_checks 430 globalDBQueries <- query 431 } 432 return qr, err 433 } 434 435 func expectDeleteQueries(t *testing.T) { 436 t.Helper() 437 expectNontxQueries(t, qh.Expect( 438 "/delete from _vt.vreplication", 439 "/delete from _vt.copy_state", 440 "/delete from _vt.post_copy_action", 441 )) 442 } 443 444 func expectLogsAndUnsubscribe(t *testing.T, logs []LogExpectation, logCh chan any) { 445 t.Helper() 446 defer vrLogStatsLogger.Unsubscribe(logCh) 447 failed := false 448 for i, log := range logs { 449 if failed { 450 t.Errorf("no logs received") 451 continue 452 } 453 select { 454 case data := <-logCh: 455 got, ok := data.(*VrLogStats) 456 if !ok { 457 t.Errorf("got not ok casting to VrLogStats: %v", data) 458 } 459 var match bool 460 match = (log.Type == got.Type) 461 if match { 462 if log.Detail[0] == '/' { 463 result, err := regexp.MatchString(log.Detail[1:], got.Detail) 464 if err != nil { 465 panic(err) 466 } 467 match = result 468 } else { 469 match = (got.Detail == log.Detail) 470 } 471 } 472 473 if !match { 474 t.Errorf("log:\n%q, does not match log %d:\n%q", got, i, log) 475 } 476 case <-time.After(5 * time.Second): 477 t.Errorf("no logs received, expecting %s", log) 478 failed = true 479 } 480 } 481 } 482 483 func shouldIgnoreQuery(query string) bool { 484 queriesToIgnore := []string{ 485 "_vt.vreplication_log", // ignore all selects, updates and inserts into this table 486 "@@session.sql_mode", // ignore all selects, and sets of this variable 487 ", time_heartbeat=", // update of last heartbeat time, can happen out-of-band, so can't test for it 488 ", time_throttled=", // update of last throttle time, can happen out-of-band, so can't test for it 489 ", component_throttled=", // update of last throttle time, can happen out-of-band, so can't test for it 490 "context cancel", 491 } 492 if sidecardb.MatchesInitQuery(query) { 493 return true 494 } 495 for _, q := range queriesToIgnore { 496 if strings.Contains(query, q) { 497 return true 498 } 499 } 500 return heartbeatRe.MatchString(query) 501 } 502 503 func expectDBClientQueries(t *testing.T, expectations qh.ExpectationSequence, skippableOnce ...string) { 504 t.Helper() 505 failed := false 506 skippedOnce := false 507 validator := qh.NewVerifier(expectations) 508 509 for len(validator.Pending()) > 0 { 510 if failed { 511 t.Errorf("no query received") 512 continue 513 } 514 var got string 515 retry: 516 select { 517 case got = <-globalDBQueries: 518 // We rule out heartbeat time update queries because otherwise our query list 519 // is indeterminable and varies with each test execution. 520 if shouldIgnoreQuery(got) { 521 goto retry 522 } 523 524 result := validator.AcceptQuery(got) 525 526 if !result.Accepted { 527 if !skippedOnce { 528 // let's see if "got" is a skippable query 529 for _, skippable := range skippableOnce { 530 if ok, _ := qh.MatchQueries(skippable, got); ok { 531 skippedOnce = true 532 goto retry 533 } 534 } 535 } 536 require.True(t, result.Accepted, fmt.Sprintf( 537 "query:%q\nmessage:%s\nexpectation:%s\nmatched:%t\nerror:%v\nhistory:%s", 538 got, result.Message, result.Expectation, result.Matched, result.Error, validator.History(), 539 )) 540 } 541 case <-time.After(5 * time.Second): 542 t.Fatalf("no query received") 543 failed = true 544 } 545 } 546 for { 547 select { 548 case got := <-globalDBQueries: 549 if shouldIgnoreQuery(got) { 550 continue 551 } 552 t.Errorf("unexpected query: %s", got) 553 default: 554 // Assert there are no pending expectations. 555 require.Len(t, validator.Pending(), 0) 556 return 557 } 558 } 559 } 560 561 // expectNontxQueries disregards transactional statements like begin and commit. 562 // It also disregards updates to _vt.vreplication. 563 func expectNontxQueries(t *testing.T, expectations qh.ExpectationSequence) { 564 t.Helper() 565 566 failed := false 567 568 validator := qh.NewVerifier(expectations) 569 570 for len(validator.Pending()) > 0 { 571 if failed { 572 t.Errorf("no query received") 573 continue 574 } 575 var got string 576 retry: 577 select { 578 case got = <-globalDBQueries: 579 if got == "begin" || got == "commit" || got == "rollback" || strings.Contains(got, "update _vt.vreplication set pos") || shouldIgnoreQuery(got) { 580 goto retry 581 } 582 583 result := validator.AcceptQuery(got) 584 585 require.True(t, result.Accepted, fmt.Sprintf( 586 "query:%q\nmessage:%s\nexpectation:%s\nmatched:%t\nerror:%v\nhistory:%s", 587 got, result.Message, result.Expectation, result.Matched, result.Error, validator.History(), 588 )) 589 case <-time.After(5 * time.Second): 590 t.Fatalf("no query received") 591 failed = true 592 } 593 } 594 for { 595 select { 596 case got := <-globalDBQueries: 597 if got == "begin" || got == "commit" || got == "rollback" || strings.Contains(got, "_vt.vreplication") { 598 continue 599 } 600 if shouldIgnoreQuery(got) { 601 continue 602 } 603 t.Errorf("unexpected query: %s", got) 604 default: 605 // Assert there are no pending expectations. 606 require.Len(t, validator.Pending(), 0) 607 return 608 } 609 } 610 } 611 612 func expectData(t *testing.T, table string, values [][]string) { 613 t.Helper() 614 customExpectData(t, table, values, env.Mysqld.FetchSuperQuery) 615 } 616 617 func expectQueryResult(t *testing.T, query string, values [][]string) { 618 t.Helper() 619 err := compareQueryResults(t, query, values, env.Mysqld.FetchSuperQuery) 620 if err != nil { 621 require.FailNow(t, "data mismatch", err) 622 } 623 } 624 625 func customExpectData(t *testing.T, table string, values [][]string, exec func(ctx context.Context, query string) (*sqltypes.Result, error)) { 626 t.Helper() 627 628 var query string 629 if len(strings.Split(table, ".")) == 1 { 630 query = fmt.Sprintf("select * from %s.%s", vrepldb, table) 631 } else { 632 query = fmt.Sprintf("select * from %s", table) 633 } 634 err := compareQueryResults(t, query, values, exec) 635 if err != nil { 636 require.FailNow(t, "data mismatch", err) 637 } 638 } 639 640 func compareQueryResults(t *testing.T, query string, values [][]string, 641 exec func(ctx context.Context, query string) (*sqltypes.Result, error)) error { 642 643 t.Helper() 644 qr, err := exec(context.Background(), query) 645 if err != nil { 646 return err 647 } 648 if len(values) != len(qr.Rows) { 649 return fmt.Errorf("row counts don't match: %v, want %v", qr.Rows, values) 650 } 651 for i, row := range values { 652 if len(row) != len(qr.Rows[i]) { 653 return fmt.Errorf("Too few columns, \nrow: %d, \nresult: %d:%v, \nwant: %d:%v", i, len(qr.Rows[i]), qr.Rows[i], len(row), row) 654 } 655 for j, val := range row { 656 if got := qr.Rows[i][j].ToString(); got != val { 657 return fmt.Errorf("Mismatch at (%d, %d): %v, want %s", i, j, qr.Rows[i][j], val) 658 } 659 } 660 } 661 662 return nil 663 } 664 665 func validateQueryCountStat(t *testing.T, phase string, want int64) { 666 var count int64 667 for _, ct := range globalStats.status().Controllers { 668 for ph, cnt := range ct.QueryCounts { 669 if ph == phase { 670 count += cnt 671 } 672 } 673 } 674 require.Equal(t, want, count, "QueryCount stat is incorrect") 675 } 676 677 func validateCopyRowCountStat(t *testing.T, want int64) { 678 var count int64 679 for _, ct := range globalStats.status().Controllers { 680 count += ct.CopyRowCount 681 } 682 require.Equal(t, want, count, "CopyRowCount stat is incorrect") 683 }