github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/applier/redo_test.go (about) 1 // Copyright 2021 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package applier 15 16 import ( 17 "context" 18 "database/sql" 19 "fmt" 20 "os" 21 "testing" 22 23 "github.com/DATA-DOG/go-sqlmock" 24 "github.com/go-sql-driver/mysql" 25 "github.com/phayes/freeport" 26 timodel "github.com/pingcap/tidb/pkg/parser/model" 27 mysqlParser "github.com/pingcap/tidb/pkg/parser/mysql" 28 "github.com/pingcap/tiflow/cdc/model" 29 "github.com/pingcap/tiflow/cdc/redo/reader" 30 mysqlDDL "github.com/pingcap/tiflow/cdc/sink/ddlsink/mysql" 31 "github.com/pingcap/tiflow/cdc/sink/dmlsink/txn" 32 pmysql "github.com/pingcap/tiflow/pkg/sink/mysql" 33 "github.com/stretchr/testify/require" 34 ) 35 36 var _ reader.RedoLogReader = &MockReader{} 37 38 // MockReader is a mock redo log reader that implements LogReader interface 39 type MockReader struct { 40 checkpointTs uint64 41 resolvedTs uint64 42 redoLogCh chan *model.RowChangedEvent 43 ddlEventCh chan *model.DDLEvent 44 } 45 46 // NewMockReader creates a new MockReader 47 func NewMockReader( 48 checkpointTs uint64, 49 resolvedTs uint64, 50 redoLogCh chan *model.RowChangedEvent, 51 ddlEventCh chan *model.DDLEvent, 52 ) *MockReader { 53 return &MockReader{ 54 checkpointTs: checkpointTs, 55 resolvedTs: resolvedTs, 56 redoLogCh: redoLogCh, 57 ddlEventCh: ddlEventCh, 58 } 59 } 60 61 // ResetReader implements LogReader.ReadLog 62 func (br *MockReader) Run(ctx context.Context) error { 63 return nil 64 } 65 66 // ReadNextRow implements LogReader.ReadNextRow 67 func (br *MockReader) ReadNextRow(ctx context.Context) (*model.RowChangedEvent, error) { 68 select { 69 case <-ctx.Done(): 70 return nil, ctx.Err() 71 case row := <-br.redoLogCh: 72 return row, nil 73 } 74 } 75 76 // ReadNextDDL implements LogReader.ReadNextDDL 77 func (br *MockReader) ReadNextDDL(ctx context.Context) (*model.DDLEvent, error) { 78 select { 79 case <-ctx.Done(): 80 return nil, ctx.Err() 81 case ddl := <-br.ddlEventCh: 82 return ddl, nil 83 } 84 } 85 86 // ReadMeta implements LogReader.ReadMeta 87 func (br *MockReader) ReadMeta(ctx context.Context) (checkpointTs, resolvedTs uint64, err error) { 88 return br.checkpointTs, br.resolvedTs, nil 89 } 90 91 func TestApply(t *testing.T) { 92 ctx, cancel := context.WithCancel(context.Background()) 93 defer cancel() 94 95 checkpointTs := uint64(1000) 96 resolvedTs := uint64(2000) 97 redoLogCh := make(chan *model.RowChangedEvent, 1024) 98 ddlEventCh := make(chan *model.DDLEvent, 1024) 99 createMockReader := func(ctx context.Context, cfg *RedoApplierConfig) (reader.RedoLogReader, error) { 100 return NewMockReader(checkpointTs, resolvedTs, redoLogCh, ddlEventCh), nil 101 } 102 103 dbIndex := 0 104 // DML sink and DDL sink share the same db 105 db := getMockDB(t) 106 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 107 defer func() { 108 dbIndex++ 109 }() 110 if dbIndex%2 == 0 { 111 testDB, err := pmysql.MockTestDB() 112 require.Nil(t, err) 113 return testDB, nil 114 } 115 return db, nil 116 } 117 118 getDMLDBConnBak := txn.GetDBConnImpl 119 txn.GetDBConnImpl = mockGetDBConn 120 getDDLDBConnBak := mysqlDDL.GetDBConnImpl 121 mysqlDDL.GetDBConnImpl = mockGetDBConn 122 createRedoReaderBak := createRedoReader 123 createRedoReader = createMockReader 124 defer func() { 125 createRedoReader = createRedoReaderBak 126 txn.GetDBConnImpl = getDMLDBConnBak 127 mysqlDDL.GetDBConnImpl = getDDLDBConnBak 128 }() 129 130 tableInfo := model.BuildTableInfo("test", "t1", []*model.Column{ 131 { 132 Name: "a", 133 Type: mysqlParser.TypeLong, 134 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 135 }, { 136 Name: "b", 137 Type: mysqlParser.TypeString, 138 Flag: 0, 139 }, 140 }, [][]int{{0}}) 141 dmls := []*model.RowChangedEvent{ 142 { 143 StartTs: 1100, 144 CommitTs: 1200, 145 TableInfo: tableInfo, 146 Columns: model.Columns2ColumnDatas([]*model.Column{ 147 { 148 Name: "a", 149 Value: 1, 150 }, { 151 Name: "b", 152 Value: "2", 153 }, 154 }, tableInfo), 155 }, 156 // update event which doesn't modify handle key 157 { 158 StartTs: 1120, 159 CommitTs: 1220, 160 TableInfo: tableInfo, 161 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 162 { 163 Name: "a", 164 Value: 1, 165 }, { 166 Name: "b", 167 Value: "2", 168 }, 169 }, tableInfo), 170 Columns: model.Columns2ColumnDatas([]*model.Column{ 171 { 172 Name: "a", 173 Value: 1, 174 }, { 175 Name: "b", 176 Value: "3", 177 }, 178 }, tableInfo), 179 }, 180 { 181 StartTs: 1150, 182 CommitTs: 1250, 183 TableInfo: tableInfo, 184 Columns: model.Columns2ColumnDatas([]*model.Column{ 185 { 186 Name: "a", 187 Value: 10, 188 }, { 189 Name: "b", 190 Value: "20", 191 }, 192 }, tableInfo), 193 }, 194 { 195 StartTs: 1150, 196 CommitTs: 1250, 197 TableInfo: tableInfo, 198 Columns: model.Columns2ColumnDatas([]*model.Column{ 199 { 200 Name: "a", 201 Value: 100, 202 }, { 203 Name: "b", 204 Value: "200", 205 }, 206 }, tableInfo), 207 }, 208 { 209 StartTs: 1200, 210 CommitTs: resolvedTs, 211 TableInfo: tableInfo, 212 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 213 { 214 Name: "a", 215 Value: 10, 216 }, { 217 Name: "b", 218 Value: "20", 219 }, 220 }, tableInfo), 221 }, 222 { 223 StartTs: 1200, 224 CommitTs: resolvedTs, 225 TableInfo: tableInfo, 226 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 227 { 228 Name: "a", 229 Value: 1, 230 }, { 231 Name: "b", 232 Value: "3", 233 }, 234 }, tableInfo), 235 Columns: model.Columns2ColumnDatas([]*model.Column{ 236 { 237 Name: "a", 238 Value: 2, 239 }, { 240 Name: "b", 241 Value: "3", 242 }, 243 }, tableInfo), 244 }, 245 { 246 StartTs: 1200, 247 CommitTs: resolvedTs, 248 TableInfo: tableInfo, 249 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 250 { 251 Name: "a", 252 Value: 100, 253 }, { 254 Name: "b", 255 Value: "200", 256 }, 257 }, tableInfo), 258 Columns: model.Columns2ColumnDatas([]*model.Column{ 259 { 260 Name: "a", 261 Value: 200, 262 }, { 263 Name: "b", 264 Value: "300", 265 }, 266 }, tableInfo), 267 }, 268 } 269 for _, dml := range dmls { 270 redoLogCh <- dml 271 } 272 ddls := []*model.DDLEvent{ 273 { 274 CommitTs: checkpointTs, 275 TableInfo: &model.TableInfo{ 276 TableName: model.TableName{ 277 Schema: "test", Table: "checkpoint", 278 }, 279 }, 280 Query: "create table checkpoint(id int)", 281 Type: timodel.ActionCreateTable, 282 }, 283 { 284 CommitTs: resolvedTs, 285 TableInfo: &model.TableInfo{ 286 TableName: model.TableName{ 287 Schema: "test", Table: "resolved", 288 }, 289 }, 290 Query: "create table resolved(id int not null unique key)", 291 Type: timodel.ActionCreateTable, 292 }, 293 } 294 for _, ddl := range ddls { 295 ddlEventCh <- ddl 296 } 297 close(redoLogCh) 298 close(ddlEventCh) 299 300 dir, err := os.Getwd() 301 require.Nil(t, err) 302 cfg := &RedoApplierConfig{ 303 SinkURI: "mysql://127.0.0.1:4000/?worker-count=1&max-txn-row=1" + 304 "&tidb_placement_mode=ignore&safe-mode=true&cache-prep-stmts=false" + 305 "&multi-stmt-enable=false", 306 Dir: dir, 307 } 308 ap := NewRedoApplier(cfg) 309 err = ap.Apply(ctx) 310 require.Nil(t, err) 311 } 312 313 func TestApplyBigTxn(t *testing.T) { 314 ctx, cancel := context.WithCancel(context.Background()) 315 defer cancel() 316 317 checkpointTs := uint64(1000) 318 resolvedTs := uint64(2000) 319 redoLogCh := make(chan *model.RowChangedEvent, 1024) 320 ddlEventCh := make(chan *model.DDLEvent, 1024) 321 createMockReader := func(ctx context.Context, cfg *RedoApplierConfig) (reader.RedoLogReader, error) { 322 return NewMockReader(checkpointTs, resolvedTs, redoLogCh, ddlEventCh), nil 323 } 324 325 dbIndex := 0 326 // DML sink and DDL sink share the same db 327 db := getMockDBForBigTxn(t) 328 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 329 defer func() { 330 dbIndex++ 331 }() 332 if dbIndex%2 == 0 { 333 testDB, err := pmysql.MockTestDB() 334 require.Nil(t, err) 335 return testDB, nil 336 } 337 return db, nil 338 } 339 340 getDMLDBConnBak := txn.GetDBConnImpl 341 txn.GetDBConnImpl = mockGetDBConn 342 getDDLDBConnBak := mysqlDDL.GetDBConnImpl 343 mysqlDDL.GetDBConnImpl = mockGetDBConn 344 createRedoReaderBak := createRedoReader 345 createRedoReader = createMockReader 346 defer func() { 347 createRedoReader = createRedoReaderBak 348 txn.GetDBConnImpl = getDMLDBConnBak 349 mysqlDDL.GetDBConnImpl = getDDLDBConnBak 350 }() 351 352 tableInfo := model.BuildTableInfo("test", "t1", []*model.Column{ 353 { 354 Name: "a", 355 Type: mysqlParser.TypeLong, 356 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 357 }, { 358 Name: "b", 359 Type: mysqlParser.TypeString, 360 Flag: 0, 361 }, 362 }, [][]int{{0}}) 363 dmls := make([]*model.RowChangedEvent, 0) 364 // insert some rows 365 for i := 1; i <= 100; i++ { 366 dml := &model.RowChangedEvent{ 367 StartTs: 1100, 368 CommitTs: 1200, 369 TableInfo: tableInfo, 370 Columns: model.Columns2ColumnDatas([]*model.Column{ 371 { 372 Name: "a", 373 Value: i, 374 }, { 375 Name: "b", 376 Value: fmt.Sprintf("%d", i+1), 377 }, 378 }, tableInfo), 379 } 380 dmls = append(dmls, dml) 381 } 382 // update 383 for i := 1; i <= 100; i++ { 384 dml := &model.RowChangedEvent{ 385 StartTs: 1200, 386 CommitTs: 1300, 387 TableInfo: tableInfo, 388 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 389 { 390 Name: "a", 391 Value: i, 392 }, { 393 Name: "b", 394 Value: fmt.Sprintf("%d", i+1), 395 }, 396 }, tableInfo), 397 Columns: model.Columns2ColumnDatas([]*model.Column{ 398 { 399 Name: "a", 400 Value: i * 10, 401 }, { 402 Name: "b", 403 Value: fmt.Sprintf("%d", i*10+1), 404 }, 405 }, tableInfo), 406 } 407 dmls = append(dmls, dml) 408 } 409 // delete and update 410 for i := 1; i <= 50; i++ { 411 dml := &model.RowChangedEvent{ 412 StartTs: 1300, 413 CommitTs: resolvedTs, 414 TableInfo: tableInfo, 415 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 416 { 417 Name: "a", 418 Value: i * 10, 419 }, { 420 Name: "b", 421 Value: fmt.Sprintf("%d", i*10+1), 422 }, 423 }, tableInfo), 424 } 425 dmls = append(dmls, dml) 426 } 427 for i := 51; i <= 100; i++ { 428 dml := &model.RowChangedEvent{ 429 StartTs: 1300, 430 CommitTs: resolvedTs, 431 TableInfo: tableInfo, 432 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 433 { 434 Name: "a", 435 Value: i * 10, 436 }, { 437 Name: "b", 438 Value: fmt.Sprintf("%d", i*10+1), 439 }, 440 }, tableInfo), 441 Columns: model.Columns2ColumnDatas([]*model.Column{ 442 { 443 Name: "a", 444 Value: i * 100, 445 }, { 446 Name: "b", 447 Value: fmt.Sprintf("%d", i*100+1), 448 }, 449 }, tableInfo), 450 } 451 dmls = append(dmls, dml) 452 } 453 for _, dml := range dmls { 454 redoLogCh <- dml 455 } 456 ddls := []*model.DDLEvent{ 457 { 458 CommitTs: checkpointTs, 459 TableInfo: &model.TableInfo{ 460 TableName: model.TableName{ 461 Schema: "test", Table: "checkpoint", 462 }, 463 }, 464 Query: "create table checkpoint(id int)", 465 Type: timodel.ActionCreateTable, 466 }, 467 { 468 CommitTs: resolvedTs, 469 TableInfo: &model.TableInfo{ 470 TableName: model.TableName{ 471 Schema: "test", Table: "resolved", 472 }, 473 }, 474 Query: "create table resolved(id int not null unique key)", 475 Type: timodel.ActionCreateTable, 476 }, 477 } 478 for _, ddl := range ddls { 479 ddlEventCh <- ddl 480 } 481 close(redoLogCh) 482 close(ddlEventCh) 483 484 dir, err := os.Getwd() 485 require.Nil(t, err) 486 cfg := &RedoApplierConfig{ 487 SinkURI: "mysql://127.0.0.1:4000/?worker-count=1&max-txn-row=1" + 488 "&tidb_placement_mode=ignore&safe-mode=true&cache-prep-stmts=false" + 489 "&multi-stmt-enable=false", 490 Dir: dir, 491 } 492 ap := NewRedoApplier(cfg) 493 err = ap.Apply(ctx) 494 require.Nil(t, err) 495 } 496 497 func TestApplyMeetSinkError(t *testing.T) { 498 ctx, cancel := context.WithCancel(context.Background()) 499 defer cancel() 500 501 port, err := freeport.GetFreePort() 502 require.Nil(t, err) 503 cfg := &RedoApplierConfig{ 504 Storage: "blackhole://", 505 SinkURI: fmt.Sprintf("mysql://127.0.0.1:%d/?read-timeout=1s&timeout=1s", port), 506 } 507 ap := NewRedoApplier(cfg) 508 err = ap.Apply(ctx) 509 require.Regexp(t, "CDC:ErrMySQLConnectionError", err) 510 } 511 512 func getMockDB(t *testing.T) *sql.DB { 513 // normal db 514 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 515 require.Nil(t, err) 516 517 // Before we write data to downstream, we need to check whether the downstream is TiDB. 518 // So we mock a select tidb_version() query. 519 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 520 Number: 1305, 521 Message: "FUNCTION test.tidb_version does not exist", 522 }) 523 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 524 Number: 1305, 525 Message: "FUNCTION test.tidb_version does not exist", 526 }) 527 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 528 Number: 1305, 529 Message: "FUNCTION test.tidb_version does not exist", 530 }) 531 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 532 Number: 1305, 533 Message: "FUNCTION test.tidb_version does not exist", 534 }) 535 536 mock.ExpectBegin() 537 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 538 mock.ExpectExec("create table checkpoint(id int)").WillReturnResult(sqlmock.NewResult(1, 1)) 539 mock.ExpectCommit() 540 541 mock.ExpectBegin() 542 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 543 WithArgs(1, "2"). 544 WillReturnResult(sqlmock.NewResult(1, 1)) 545 mock.ExpectCommit() 546 547 mock.ExpectBegin() 548 mock.ExpectExec("UPDATE `test`.`t1` SET `a` = ?, `b` = ? WHERE `a` = ? LIMIT 1"). 549 WithArgs(1, "3", 1). 550 WillReturnResult(sqlmock.NewResult(1, 1)) 551 mock.ExpectCommit() 552 553 mock.ExpectBegin() 554 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 555 WithArgs(10, "20"). 556 WillReturnResult(sqlmock.NewResult(1, 1)) 557 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 558 WithArgs(100, "200"). 559 WillReturnResult(sqlmock.NewResult(1, 1)) 560 mock.ExpectCommit() 561 562 // First, apply row which commitTs equal to resolvedTs 563 mock.ExpectBegin() 564 mock.ExpectExec("DELETE FROM `test`.`t1` WHERE (`a` = ?)"). 565 WithArgs(10). 566 WillReturnResult(sqlmock.NewResult(1, 1)) 567 mock.ExpectExec("DELETE FROM `test`.`t1` WHERE (`a` = ?)"). 568 WithArgs(1). 569 WillReturnResult(sqlmock.NewResult(1, 1)) 570 mock.ExpectExec("DELETE FROM `test`.`t1` WHERE (`a` = ?)"). 571 WithArgs(100). 572 WillReturnResult(sqlmock.NewResult(1, 1)) 573 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 574 WithArgs(2, "3"). 575 WillReturnResult(sqlmock.NewResult(1, 1)) 576 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 577 WithArgs(200, "300"). 578 WillReturnResult(sqlmock.NewResult(1, 1)) 579 mock.ExpectCommit() 580 581 // Then, apply ddl which commitTs equal to resolvedTs 582 mock.ExpectBegin() 583 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 584 mock.ExpectExec("create table resolved(id int not null unique key)").WillReturnResult(sqlmock.NewResult(1, 1)) 585 mock.ExpectCommit() 586 587 mock.ExpectClose() 588 return db 589 } 590 591 func getMockDBForBigTxn(t *testing.T) *sql.DB { 592 // normal db 593 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 594 require.Nil(t, err) 595 596 // Before we write data to downstream, we need to check whether the downstream is TiDB. 597 // So we mock a select tidb_version() query. 598 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 599 Number: 1305, 600 Message: "FUNCTION test.tidb_version does not exist", 601 }) 602 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 603 Number: 1305, 604 Message: "FUNCTION test.tidb_version does not exist", 605 }) 606 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 607 Number: 1305, 608 Message: "FUNCTION test.tidb_version does not exist", 609 }) 610 mock.ExpectQuery("select tidb_version()").WillReturnError(&mysql.MySQLError{ 611 Number: 1305, 612 Message: "FUNCTION test.tidb_version does not exist", 613 }) 614 615 mock.ExpectBegin() 616 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 617 mock.ExpectExec("create table checkpoint(id int)").WillReturnResult(sqlmock.NewResult(1, 1)) 618 mock.ExpectCommit() 619 620 mock.ExpectBegin() 621 for i := 1; i <= 100; i++ { 622 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 623 WithArgs(i, fmt.Sprintf("%d", i+1)). 624 WillReturnResult(sqlmock.NewResult(1, 1)) 625 } 626 mock.ExpectCommit() 627 628 mock.ExpectBegin() 629 for i := 1; i <= 100; i++ { 630 mock.ExpectExec("DELETE FROM `test`.`t1` WHERE (`a` = ?)"). 631 WithArgs(i). 632 WillReturnResult(sqlmock.NewResult(1, 1)) 633 } 634 for i := 1; i <= 100; i++ { 635 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 636 WithArgs(i*10, fmt.Sprintf("%d", i*10+1)). 637 WillReturnResult(sqlmock.NewResult(1, 1)) 638 } 639 mock.ExpectCommit() 640 641 // First, apply row which commitTs equal to resolvedTs 642 mock.ExpectBegin() 643 for i := 1; i <= 100; i++ { 644 mock.ExpectExec("DELETE FROM `test`.`t1` WHERE (`a` = ?)"). 645 WithArgs(i * 10). 646 WillReturnResult(sqlmock.NewResult(1, 1)) 647 } 648 for i := 51; i <= 100; i++ { 649 mock.ExpectExec("REPLACE INTO `test`.`t1` (`a`,`b`) VALUES (?,?)"). 650 WithArgs(i*100, fmt.Sprintf("%d", i*100+1)). 651 WillReturnResult(sqlmock.NewResult(1, 1)) 652 } 653 mock.ExpectCommit() 654 655 // Then, apply ddl which commitTs equal to resolvedTs 656 mock.ExpectBegin() 657 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 658 mock.ExpectExec("create table resolved(id int not null unique key)").WillReturnResult(sqlmock.NewResult(1, 1)) 659 mock.ExpectCommit() 660 661 mock.ExpectClose() 662 return db 663 }