github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cdc/sink/mysql_test.go (about) 1 // Copyright 2020 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 sink 15 16 import ( 17 "context" 18 "database/sql" 19 "database/sql/driver" 20 "fmt" 21 "net" 22 "net/url" 23 "sort" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 "github.com/DATA-DOG/go-sqlmock" 30 "github.com/davecgh/go-spew/spew" 31 dmysql "github.com/go-sql-driver/mysql" 32 "github.com/pingcap/check" 33 "github.com/pingcap/errors" 34 timodel "github.com/pingcap/parser/model" 35 "github.com/pingcap/parser/mysql" 36 "github.com/pingcap/ticdc/cdc/model" 37 "github.com/pingcap/ticdc/cdc/sink/common" 38 "github.com/pingcap/ticdc/pkg/config" 39 "github.com/pingcap/ticdc/pkg/cyclic/mark" 40 cerror "github.com/pingcap/ticdc/pkg/errors" 41 "github.com/pingcap/ticdc/pkg/filter" 42 "github.com/pingcap/ticdc/pkg/notify" 43 "github.com/pingcap/ticdc/pkg/retry" 44 "github.com/pingcap/ticdc/pkg/util/testleak" 45 "github.com/pingcap/tidb/infoschema" 46 "golang.org/x/sync/errgroup" 47 ) 48 49 type MySQLSinkSuite struct{} 50 51 func Test(t *testing.T) { check.TestingT(t) } 52 53 var _ = check.Suite(&MySQLSinkSuite{}) 54 55 func newMySQLSink4Test(ctx context.Context, c *check.C) *mysqlSink { 56 f, err := filter.NewFilter(config.GetDefaultReplicaConfig()) 57 c.Assert(err, check.IsNil) 58 params := defaultParams.Clone() 59 params.batchReplaceEnabled = false 60 return &mysqlSink{ 61 txnCache: common.NewUnresolvedTxnCache(), 62 filter: f, 63 statistics: NewStatistics(ctx, "test", make(map[string]string)), 64 params: params, 65 } 66 } 67 68 func (s MySQLSinkSuite) TestMysqlSinkWorker(c *check.C) { 69 defer testleak.AfterTest(c)() 70 testCases := []struct { 71 txns []*model.SingleTableTxn 72 expectedOutputRows [][]*model.RowChangedEvent 73 exportedOutputReplicaIDs []uint64 74 maxTxnRow int 75 }{ 76 { 77 txns: []*model.SingleTableTxn{}, 78 maxTxnRow: 4, 79 }, { 80 txns: []*model.SingleTableTxn{ 81 { 82 CommitTs: 1, 83 Rows: []*model.RowChangedEvent{{CommitTs: 1}}, 84 ReplicaID: 1, 85 }, 86 }, 87 expectedOutputRows: [][]*model.RowChangedEvent{{{CommitTs: 1}}}, 88 exportedOutputReplicaIDs: []uint64{1}, 89 maxTxnRow: 2, 90 }, { 91 txns: []*model.SingleTableTxn{ 92 { 93 CommitTs: 1, 94 Rows: []*model.RowChangedEvent{{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 1}}, 95 ReplicaID: 1, 96 }, 97 }, 98 expectedOutputRows: [][]*model.RowChangedEvent{ 99 {{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 1}}, 100 }, 101 exportedOutputReplicaIDs: []uint64{1}, 102 maxTxnRow: 2, 103 }, { 104 txns: []*model.SingleTableTxn{ 105 { 106 CommitTs: 1, 107 Rows: []*model.RowChangedEvent{{CommitTs: 1}, {CommitTs: 1}}, 108 ReplicaID: 1, 109 }, 110 { 111 CommitTs: 2, 112 Rows: []*model.RowChangedEvent{{CommitTs: 2}}, 113 ReplicaID: 1, 114 }, 115 { 116 CommitTs: 3, 117 Rows: []*model.RowChangedEvent{{CommitTs: 3}, {CommitTs: 3}}, 118 ReplicaID: 1, 119 }, 120 }, 121 expectedOutputRows: [][]*model.RowChangedEvent{ 122 {{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 2}}, 123 {{CommitTs: 3}, {CommitTs: 3}}, 124 }, 125 exportedOutputReplicaIDs: []uint64{1, 1}, 126 maxTxnRow: 4, 127 }, { 128 txns: []*model.SingleTableTxn{ 129 { 130 CommitTs: 1, 131 Rows: []*model.RowChangedEvent{{CommitTs: 1}}, 132 ReplicaID: 1, 133 }, 134 { 135 CommitTs: 2, 136 Rows: []*model.RowChangedEvent{{CommitTs: 2}}, 137 ReplicaID: 2, 138 }, 139 { 140 CommitTs: 3, 141 Rows: []*model.RowChangedEvent{{CommitTs: 3}}, 142 ReplicaID: 3, 143 }, 144 }, 145 expectedOutputRows: [][]*model.RowChangedEvent{ 146 {{CommitTs: 1}}, 147 {{CommitTs: 2}}, 148 {{CommitTs: 3}}, 149 }, 150 exportedOutputReplicaIDs: []uint64{1, 2, 3}, 151 maxTxnRow: 4, 152 }, { 153 txns: []*model.SingleTableTxn{ 154 { 155 CommitTs: 1, 156 Rows: []*model.RowChangedEvent{{CommitTs: 1}}, 157 ReplicaID: 1, 158 }, 159 { 160 CommitTs: 2, 161 Rows: []*model.RowChangedEvent{{CommitTs: 2}, {CommitTs: 2}, {CommitTs: 2}}, 162 ReplicaID: 1, 163 }, 164 { 165 CommitTs: 3, 166 Rows: []*model.RowChangedEvent{{CommitTs: 3}}, 167 ReplicaID: 1, 168 }, 169 { 170 CommitTs: 4, 171 Rows: []*model.RowChangedEvent{{CommitTs: 4}}, 172 ReplicaID: 1, 173 }, 174 }, 175 expectedOutputRows: [][]*model.RowChangedEvent{ 176 {{CommitTs: 1}}, 177 {{CommitTs: 2}, {CommitTs: 2}, {CommitTs: 2}}, 178 {{CommitTs: 3}, {CommitTs: 4}}, 179 }, 180 exportedOutputReplicaIDs: []uint64{1, 1, 1}, 181 maxTxnRow: 2, 182 }, 183 } 184 ctx := context.Background() 185 186 notifier := new(notify.Notifier) 187 for i, tc := range testCases { 188 cctx, cancel := context.WithCancel(ctx) 189 var outputRows [][]*model.RowChangedEvent 190 var outputReplicaIDs []uint64 191 receiver, err := notifier.NewReceiver(-1) 192 c.Assert(err, check.IsNil) 193 w := newMySQLSinkWorker(tc.maxTxnRow, 1, 194 bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"), 195 receiver, 196 func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error { 197 outputRows = append(outputRows, events) 198 outputReplicaIDs = append(outputReplicaIDs, replicaID) 199 return nil 200 }) 201 errg, cctx := errgroup.WithContext(cctx) 202 errg.Go(func() error { 203 return w.run(cctx) 204 }) 205 for _, txn := range tc.txns { 206 w.appendTxn(cctx, txn) 207 } 208 var wg sync.WaitGroup 209 w.appendFinishTxn(&wg) 210 // ensure all txns are fetched from txn channel in sink worker 211 time.Sleep(time.Millisecond * 100) 212 notifier.Notify() 213 wg.Wait() 214 cancel() 215 c.Assert(errors.Cause(errg.Wait()), check.Equals, context.Canceled) 216 c.Assert(outputRows, check.DeepEquals, tc.expectedOutputRows, 217 check.Commentf("case %v, %s, %s", i, spew.Sdump(outputRows), spew.Sdump(tc.expectedOutputRows))) 218 c.Assert(outputReplicaIDs, check.DeepEquals, tc.exportedOutputReplicaIDs, 219 check.Commentf("case %v, %s, %s", i, spew.Sdump(outputReplicaIDs), spew.Sdump(tc.exportedOutputReplicaIDs))) 220 } 221 } 222 223 func (s MySQLSinkSuite) TestMySQLSinkWorkerExitWithError(c *check.C) { 224 defer testleak.AfterTest(c)() 225 txns1 := []*model.SingleTableTxn{ 226 { 227 CommitTs: 1, 228 Rows: []*model.RowChangedEvent{{CommitTs: 1}}, 229 }, 230 { 231 CommitTs: 2, 232 Rows: []*model.RowChangedEvent{{CommitTs: 2}}, 233 }, 234 { 235 CommitTs: 3, 236 Rows: []*model.RowChangedEvent{{CommitTs: 3}}, 237 }, 238 { 239 CommitTs: 4, 240 Rows: []*model.RowChangedEvent{{CommitTs: 4}}, 241 }, 242 } 243 txns2 := []*model.SingleTableTxn{ 244 { 245 CommitTs: 5, 246 Rows: []*model.RowChangedEvent{{CommitTs: 5}}, 247 }, 248 { 249 CommitTs: 6, 250 Rows: []*model.RowChangedEvent{{CommitTs: 6}}, 251 }, 252 } 253 maxTxnRow := 1 254 ctx := context.Background() 255 256 errExecFailed := errors.New("sink worker exec failed") 257 notifier := new(notify.Notifier) 258 cctx, cancel := context.WithCancel(ctx) 259 receiver, err := notifier.NewReceiver(-1) 260 c.Assert(err, check.IsNil) 261 w := newMySQLSinkWorker(maxTxnRow, 1, /*bucket*/ 262 bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"), 263 receiver, 264 func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error { 265 return errExecFailed 266 }) 267 errg, cctx := errgroup.WithContext(cctx) 268 errg.Go(func() error { 269 return w.run(cctx) 270 }) 271 // txn in txns1 will be sent to worker txnCh 272 for _, txn := range txns1 { 273 w.appendTxn(cctx, txn) 274 } 275 276 // simulate notify sink worker to flush existing txns 277 var wg sync.WaitGroup 278 w.appendFinishTxn(&wg) 279 time.Sleep(time.Millisecond * 100) 280 // txn in txn2 will be blocked since the worker has exited 281 for _, txn := range txns2 { 282 w.appendTxn(cctx, txn) 283 } 284 notifier.Notify() 285 286 // simulate sink shutdown and send closed singal to sink worker 287 w.closedCh <- struct{}{} 288 w.cleanup() 289 290 // the flush notification wait group should be done 291 wg.Wait() 292 293 cancel() 294 c.Assert(errg.Wait(), check.Equals, errExecFailed) 295 } 296 297 func (s MySQLSinkSuite) TestMySQLSinkWorkerExitCleanup(c *check.C) { 298 defer testleak.AfterTest(c)() 299 txns1 := []*model.SingleTableTxn{ 300 { 301 CommitTs: 1, 302 Rows: []*model.RowChangedEvent{{CommitTs: 1}}, 303 }, 304 { 305 CommitTs: 2, 306 Rows: []*model.RowChangedEvent{{CommitTs: 2}}, 307 }, 308 } 309 txns2 := []*model.SingleTableTxn{ 310 { 311 CommitTs: 5, 312 Rows: []*model.RowChangedEvent{{CommitTs: 5}}, 313 }, 314 } 315 316 maxTxnRow := 1 317 ctx := context.Background() 318 319 errExecFailed := errors.New("sink worker exec failed") 320 notifier := new(notify.Notifier) 321 cctx, cancel := context.WithCancel(ctx) 322 receiver, err := notifier.NewReceiver(-1) 323 c.Assert(err, check.IsNil) 324 w := newMySQLSinkWorker(maxTxnRow, 1, /*bucket*/ 325 bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"), 326 receiver, 327 func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error { 328 return errExecFailed 329 }) 330 errg, cctx := errgroup.WithContext(cctx) 331 errg.Go(func() error { 332 err := w.run(cctx) 333 return err 334 }) 335 for _, txn := range txns1 { 336 w.appendTxn(cctx, txn) 337 } 338 339 // sleep to let txns flushed by tick 340 time.Sleep(time.Millisecond * 100) 341 342 // simulate more txns are sent to txnCh after the sink worker run has exited 343 for _, txn := range txns2 { 344 w.appendTxn(cctx, txn) 345 } 346 var wg sync.WaitGroup 347 w.appendFinishTxn(&wg) 348 notifier.Notify() 349 350 // simulate sink shutdown and send closed singal to sink worker 351 w.closedCh <- struct{}{} 352 w.cleanup() 353 354 // the flush notification wait group should be done 355 wg.Wait() 356 357 cancel() 358 c.Assert(errg.Wait(), check.Equals, errExecFailed) 359 } 360 361 func (s MySQLSinkSuite) TestPrepareDML(c *check.C) { 362 defer testleak.AfterTest(c)() 363 testCases := []struct { 364 input []*model.RowChangedEvent 365 expected *preparedDMLs 366 }{{ 367 input: []*model.RowChangedEvent{}, 368 expected: &preparedDMLs{sqls: []string{}, values: [][]interface{}{}}, 369 }, { 370 input: []*model.RowChangedEvent{ 371 { 372 StartTs: 418658114257813514, 373 CommitTs: 418658114257813515, 374 Table: &model.TableName{Schema: "common_1", Table: "uk_without_pk"}, 375 PreColumns: []*model.Column{nil, { 376 Name: "a1", 377 Type: mysql.TypeLong, 378 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag, 379 Value: 1, 380 }, { 381 Name: "a3", 382 Type: mysql.TypeLong, 383 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag, 384 Value: 1, 385 }}, 386 IndexColumns: [][]int{{1, 2}}, 387 }, 388 }, 389 expected: &preparedDMLs{ 390 sqls: []string{"DELETE FROM `common_1`.`uk_without_pk` WHERE `a1` = ? AND `a3` = ? LIMIT 1;"}, 391 values: [][]interface{}{{1, 1}}, 392 rowCount: 1, 393 }, 394 }} 395 ctx, cancel := context.WithCancel(context.Background()) 396 defer cancel() 397 ms := newMySQLSink4Test(ctx, c) 398 for i, tc := range testCases { 399 dmls := ms.prepareDMLs(tc.input, 0, 0) 400 c.Assert(dmls, check.DeepEquals, tc.expected, check.Commentf("%d", i)) 401 } 402 } 403 404 func (s MySQLSinkSuite) TestPrepareUpdate(c *check.C) { 405 defer testleak.AfterTest(c)() 406 testCases := []struct { 407 quoteTable string 408 preCols []*model.Column 409 cols []*model.Column 410 expectedSQL string 411 expectedArgs []interface{} 412 }{ 413 { 414 quoteTable: "`test`.`t1`", 415 preCols: []*model.Column{}, 416 cols: []*model.Column{}, 417 expectedSQL: "", 418 expectedArgs: nil, 419 }, 420 { 421 quoteTable: "`test`.`t1`", 422 preCols: []*model.Column{ 423 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 424 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 425 }, 426 cols: []*model.Column{ 427 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 428 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test2"}, 429 }, 430 expectedSQL: "UPDATE `test`.`t1` SET `a`=?,`b`=? WHERE `a`=? LIMIT 1;", 431 expectedArgs: []interface{}{1, "test2", 1}, 432 }, 433 { 434 quoteTable: "`test`.`t1`", 435 preCols: []*model.Column{ 436 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1}, 437 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"}, 438 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 439 }, 440 cols: []*model.Column{ 441 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 2}, 442 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test2"}, 443 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 444 }, 445 expectedSQL: "UPDATE `test`.`t1` SET `a`=?,`b`=? WHERE `a`=? AND `b`=? LIMIT 1;", 446 expectedArgs: []interface{}{2, "test2", 1, "test"}, 447 }, 448 } 449 for _, tc := range testCases { 450 query, args := prepareUpdate(tc.quoteTable, tc.preCols, tc.cols, false) 451 c.Assert(query, check.Equals, tc.expectedSQL) 452 c.Assert(args, check.DeepEquals, tc.expectedArgs) 453 } 454 } 455 456 func (s MySQLSinkSuite) TestPrepareDelete(c *check.C) { 457 defer testleak.AfterTest(c)() 458 testCases := []struct { 459 quoteTable string 460 preCols []*model.Column 461 expectedSQL string 462 expectedArgs []interface{} 463 }{ 464 { 465 quoteTable: "`test`.`t1`", 466 preCols: []*model.Column{}, 467 expectedSQL: "", 468 expectedArgs: nil, 469 }, 470 { 471 quoteTable: "`test`.`t1`", 472 preCols: []*model.Column{ 473 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 474 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 475 }, 476 expectedSQL: "DELETE FROM `test`.`t1` WHERE `a` = ? LIMIT 1;", 477 expectedArgs: []interface{}{1}, 478 }, 479 { 480 quoteTable: "`test`.`t1`", 481 preCols: []*model.Column{ 482 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1}, 483 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"}, 484 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 485 }, 486 expectedSQL: "DELETE FROM `test`.`t1` WHERE `a` = ? AND `b` = ? LIMIT 1;", 487 expectedArgs: []interface{}{1, "test"}, 488 }, 489 } 490 for _, tc := range testCases { 491 query, args := prepareDelete(tc.quoteTable, tc.preCols, false) 492 c.Assert(query, check.Equals, tc.expectedSQL) 493 c.Assert(args, check.DeepEquals, tc.expectedArgs) 494 } 495 } 496 497 func (s MySQLSinkSuite) TestWhereSlice(c *check.C) { 498 defer testleak.AfterTest(c)() 499 testCases := []struct { 500 cols []*model.Column 501 forceReplicate bool 502 expectedColNames []string 503 expectedArgs []interface{} 504 }{ 505 { 506 cols: []*model.Column{}, 507 forceReplicate: false, 508 expectedColNames: nil, 509 expectedArgs: nil, 510 }, 511 { 512 cols: []*model.Column{ 513 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 514 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 515 }, 516 forceReplicate: false, 517 expectedColNames: []string{"a"}, 518 expectedArgs: []interface{}{1}, 519 }, 520 { 521 cols: []*model.Column{ 522 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1}, 523 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"}, 524 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 525 }, 526 forceReplicate: false, 527 expectedColNames: []string{"a", "b"}, 528 expectedArgs: []interface{}{1, "test"}, 529 }, 530 { 531 cols: []*model.Column{}, 532 forceReplicate: true, 533 expectedColNames: []string{}, 534 expectedArgs: []interface{}{}, 535 }, 536 { 537 cols: []*model.Column{ 538 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 539 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 540 }, 541 forceReplicate: true, 542 expectedColNames: []string{"a"}, 543 expectedArgs: []interface{}{1}, 544 }, 545 { 546 cols: []*model.Column{ 547 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1}, 548 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"}, 549 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 550 }, 551 forceReplicate: true, 552 expectedColNames: []string{"a", "b"}, 553 expectedArgs: []interface{}{1, "test"}, 554 }, 555 { 556 cols: []*model.Column{ 557 {Name: "a", Type: mysql.TypeLong, Flag: model.UniqueKeyFlag, Value: 1}, 558 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 559 }, 560 forceReplicate: true, 561 expectedColNames: []string{"a", "b"}, 562 expectedArgs: []interface{}{1, "test"}, 563 }, 564 { 565 cols: []*model.Column{ 566 {Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag, Value: 1}, 567 {Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag, Value: "test"}, 568 {Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100}, 569 }, 570 forceReplicate: true, 571 expectedColNames: []string{"a", "b", "c"}, 572 expectedArgs: []interface{}{1, "test", 100}, 573 }, 574 } 575 for _, tc := range testCases { 576 colNames, args := whereSlice(tc.cols, tc.forceReplicate) 577 c.Assert(colNames, check.DeepEquals, tc.expectedColNames) 578 c.Assert(args, check.DeepEquals, tc.expectedArgs) 579 } 580 } 581 582 func (s MySQLSinkSuite) TestMapReplace(c *check.C) { 583 defer testleak.AfterTest(c)() 584 testCases := []struct { 585 quoteTable string 586 cols []*model.Column 587 expectedQuery string 588 expectedArgs []interface{} 589 }{ 590 { 591 quoteTable: "`test`.`t1`", 592 cols: []*model.Column{ 593 {Name: "a", Type: mysql.TypeLong, Value: 1}, 594 {Name: "b", Type: mysql.TypeVarchar, Value: "varchar"}, 595 {Name: "c", Type: mysql.TypeLong, Value: 1, Flag: model.GeneratedColumnFlag}, 596 {Name: "d", Type: mysql.TypeTiny, Value: uint8(255)}, 597 }, 598 expectedQuery: "REPLACE INTO `test`.`t1`(`a`,`b`,`d`) VALUES ", 599 expectedArgs: []interface{}{1, "varchar", uint8(255)}, 600 }, 601 { 602 quoteTable: "`test`.`t1`", 603 cols: []*model.Column{ 604 {Name: "a", Type: mysql.TypeLong, Value: 1}, 605 {Name: "b", Type: mysql.TypeVarchar, Value: "varchar"}, 606 {Name: "c", Type: mysql.TypeLong, Value: 1}, 607 {Name: "d", Type: mysql.TypeTiny, Value: uint8(255)}, 608 }, 609 expectedQuery: "REPLACE INTO `test`.`t1`(`a`,`b`,`c`,`d`) VALUES ", 610 expectedArgs: []interface{}{1, "varchar", 1, uint8(255)}, 611 }, 612 } 613 for _, tc := range testCases { 614 // multiple times to verify the stability of column sequence in query string 615 for i := 0; i < 10; i++ { 616 query, args := prepareReplace(tc.quoteTable, tc.cols, false, false) 617 c.Assert(query, check.Equals, tc.expectedQuery) 618 c.Assert(args, check.DeepEquals, tc.expectedArgs) 619 } 620 } 621 } 622 623 type sqlArgs [][]interface{} 624 625 func (a sqlArgs) Len() int { return len(a) } 626 func (a sqlArgs) Less(i, j int) bool { return fmt.Sprintf("%s", a[i]) < fmt.Sprintf("%s", a[j]) } 627 func (a sqlArgs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 628 629 func (s MySQLSinkSuite) TestReduceReplace(c *check.C) { 630 defer testleak.AfterTest(c)() 631 testCases := []struct { 632 replaces map[string][][]interface{} 633 batchSize int 634 sort bool 635 expectSQLs []string 636 expectArgs [][]interface{} 637 }{ 638 { 639 replaces: map[string][][]interface{}{ 640 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": { 641 []interface{}{1, "1"}, 642 []interface{}{2, "2"}, 643 []interface{}{3, "3"}, 644 }, 645 }, 646 batchSize: 1, 647 sort: false, 648 expectSQLs: []string{ 649 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)", 650 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)", 651 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)", 652 }, 653 expectArgs: [][]interface{}{ 654 {1, "1"}, 655 {2, "2"}, 656 {3, "3"}, 657 }, 658 }, 659 { 660 replaces: map[string][][]interface{}{ 661 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": { 662 []interface{}{1, "1"}, 663 []interface{}{2, "2"}, 664 []interface{}{3, "3"}, 665 []interface{}{4, "3"}, 666 []interface{}{5, "5"}, 667 }, 668 }, 669 batchSize: 3, 670 sort: false, 671 expectSQLs: []string{ 672 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)", 673 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?)", 674 }, 675 expectArgs: [][]interface{}{ 676 {1, "1", 2, "2", 3, "3"}, 677 {4, "3", 5, "5"}, 678 }, 679 }, 680 { 681 replaces: map[string][][]interface{}{ 682 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": { 683 []interface{}{1, "1"}, 684 []interface{}{2, "2"}, 685 []interface{}{3, "3"}, 686 []interface{}{4, "3"}, 687 []interface{}{5, "5"}, 688 }, 689 }, 690 batchSize: 10, 691 sort: false, 692 expectSQLs: []string{ 693 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?),(?,?),(?,?)", 694 }, 695 expectArgs: [][]interface{}{ 696 {1, "1", 2, "2", 3, "3", 4, "3", 5, "5"}, 697 }, 698 }, 699 { 700 replaces: map[string][][]interface{}{ 701 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": { 702 []interface{}{1, "1"}, 703 []interface{}{2, "2"}, 704 []interface{}{3, "3"}, 705 []interface{}{4, "3"}, 706 []interface{}{5, "5"}, 707 []interface{}{6, "6"}, 708 }, 709 "REPLACE INTO `test`.`t2`(`a`,`b`) VALUES ": { 710 []interface{}{7, ""}, 711 []interface{}{8, ""}, 712 []interface{}{9, ""}, 713 }, 714 }, 715 batchSize: 3, 716 sort: true, 717 expectSQLs: []string{ 718 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)", 719 "REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)", 720 "REPLACE INTO `test`.`t2`(`a`,`b`) VALUES (?,?),(?,?),(?,?)", 721 }, 722 expectArgs: [][]interface{}{ 723 {1, "1", 2, "2", 3, "3"}, 724 {4, "3", 5, "5", 6, "6"}, 725 {7, "", 8, "", 9, ""}, 726 }, 727 }, 728 } 729 for _, tc := range testCases { 730 sqls, args := reduceReplace(tc.replaces, tc.batchSize) 731 if tc.sort { 732 sort.Strings(sqls) 733 sort.Sort(sqlArgs(args)) 734 } 735 c.Assert(sqls, check.DeepEquals, tc.expectSQLs) 736 c.Assert(args, check.DeepEquals, tc.expectArgs) 737 } 738 } 739 740 func (s MySQLSinkSuite) TestSinkParamsClone(c *check.C) { 741 defer testleak.AfterTest(c)() 742 param1 := defaultParams.Clone() 743 param2 := param1.Clone() 744 param2.changefeedID = "123" 745 param2.batchReplaceEnabled = false 746 param2.maxTxnRow = 1 747 c.Assert(param1, check.DeepEquals, &sinkParams{ 748 workerCount: defaultWorkerCount, 749 maxTxnRow: defaultMaxTxnRow, 750 tidbTxnMode: defaultTiDBTxnMode, 751 batchReplaceEnabled: defaultBatchReplaceEnabled, 752 batchReplaceSize: defaultBatchReplaceSize, 753 readTimeout: defaultReadTimeout, 754 writeTimeout: defaultWriteTimeout, 755 dialTimeout: defaultDialTimeout, 756 safeMode: defaultSafeMode, 757 }) 758 c.Assert(param2, check.DeepEquals, &sinkParams{ 759 changefeedID: "123", 760 workerCount: defaultWorkerCount, 761 maxTxnRow: 1, 762 tidbTxnMode: defaultTiDBTxnMode, 763 batchReplaceEnabled: false, 764 batchReplaceSize: defaultBatchReplaceSize, 765 readTimeout: defaultReadTimeout, 766 writeTimeout: defaultWriteTimeout, 767 dialTimeout: defaultDialTimeout, 768 safeMode: defaultSafeMode, 769 }) 770 } 771 772 func (s MySQLSinkSuite) TestConfigureSinkURI(c *check.C) { 773 defer testleak.AfterTest(c)() 774 775 testDefaultParams := func() { 776 db, err := mockTestDB() 777 c.Assert(err, check.IsNil) 778 defer db.Close() 779 780 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 781 c.Assert(err, check.IsNil) 782 params := defaultParams.Clone() 783 dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db) 784 c.Assert(err, check.IsNil) 785 expectedParams := []string{ 786 "tidb_txn_mode=optimistic", 787 "readTimeout=2m", 788 "writeTimeout=2m", 789 "allow_auto_random_explicit_insert=1", 790 } 791 for _, param := range expectedParams { 792 c.Assert(strings.Contains(dsnStr, param), check.IsTrue) 793 } 794 c.Assert(strings.Contains(dsnStr, "time_zone"), check.IsFalse) 795 } 796 797 testTimezoneParam := func() { 798 db, err := mockTestDB() 799 c.Assert(err, check.IsNil) 800 defer db.Close() 801 802 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 803 c.Assert(err, check.IsNil) 804 params := defaultParams.Clone() 805 params.timezone = `"UTC"` 806 dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db) 807 c.Assert(err, check.IsNil) 808 c.Assert(strings.Contains(dsnStr, "time_zone=%22UTC%22"), check.IsTrue) 809 } 810 811 testTimeoutParams := func() { 812 db, err := mockTestDB() 813 c.Assert(err, check.IsNil) 814 defer db.Close() 815 816 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 817 c.Assert(err, check.IsNil) 818 uri, err := url.Parse("mysql://127.0.0.1:3306/?read-timeout=4m&write-timeout=5m&timeout=3m") 819 c.Assert(err, check.IsNil) 820 params, err := parseSinkURI(context.TODO(), uri, map[string]string{}) 821 c.Assert(err, check.IsNil) 822 dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db) 823 c.Assert(err, check.IsNil) 824 expectedParams := []string{ 825 "readTimeout=4m", 826 "writeTimeout=5m", 827 "timeout=3m", 828 } 829 for _, param := range expectedParams { 830 c.Assert(strings.Contains(dsnStr, param), check.IsTrue) 831 } 832 } 833 834 testDefaultParams() 835 testTimezoneParam() 836 testTimeoutParams() 837 } 838 839 func (s MySQLSinkSuite) TestParseSinkURI(c *check.C) { 840 defer testleak.AfterTest(c)() 841 expected := defaultParams.Clone() 842 expected.workerCount = 64 843 expected.maxTxnRow = 20 844 expected.batchReplaceEnabled = true 845 expected.batchReplaceSize = 50 846 expected.safeMode = true 847 expected.timezone = `"UTC"` 848 expected.changefeedID = "cf-id" 849 expected.captureAddr = "127.0.0.1:8300" 850 expected.tidbTxnMode = "pessimistic" 851 uriStr := "mysql://127.0.0.1:3306/?worker-count=64&max-txn-row=20" + 852 "&batch-replace-enable=true&batch-replace-size=50&safe-mode=true" + 853 "&tidb-txn-mode=pessimistic" 854 opts := map[string]string{ 855 OptChangefeedID: expected.changefeedID, 856 OptCaptureAddr: expected.captureAddr, 857 } 858 uri, err := url.Parse(uriStr) 859 c.Assert(err, check.IsNil) 860 params, err := parseSinkURI(context.TODO(), uri, opts) 861 c.Assert(err, check.IsNil) 862 c.Assert(params, check.DeepEquals, expected) 863 } 864 865 func (s MySQLSinkSuite) TestParseSinkURITimezone(c *check.C) { 866 defer testleak.AfterTest(c)() 867 uris := []string{ 868 "mysql://127.0.0.1:3306/?time-zone=Asia/Shanghai&worker-count=32", 869 "mysql://127.0.0.1:3306/?time-zone=&worker-count=32", 870 "mysql://127.0.0.1:3306/?worker-count=32", 871 } 872 expected := []string{ 873 "\"Asia/Shanghai\"", 874 "", 875 "\"UTC\"", 876 } 877 ctx := context.TODO() 878 opts := map[string]string{} 879 for i, uriStr := range uris { 880 uri, err := url.Parse(uriStr) 881 c.Assert(err, check.IsNil) 882 params, err := parseSinkURI(ctx, uri, opts) 883 c.Assert(err, check.IsNil) 884 c.Assert(params.timezone, check.Equals, expected[i]) 885 } 886 } 887 888 func (s MySQLSinkSuite) TestParseSinkURIBadQueryString(c *check.C) { 889 defer testleak.AfterTest(c)() 890 uris := []string{ 891 "", 892 "postgre://127.0.0.1:3306", 893 "mysql://127.0.0.1:3306/?worker-count=not-number", 894 "mysql://127.0.0.1:3306/?max-txn-row=not-number", 895 "mysql://127.0.0.1:3306/?ssl-ca=only-ca-exists", 896 "mysql://127.0.0.1:3306/?batch-replace-enable=not-bool", 897 "mysql://127.0.0.1:3306/?batch-replace-enable=true&batch-replace-size=not-number", 898 "mysql://127.0.0.1:3306/?safe-mode=not-bool", 899 } 900 ctx := context.TODO() 901 opts := map[string]string{OptChangefeedID: "changefeed-01"} 902 var uri *url.URL 903 var err error 904 for _, uriStr := range uris { 905 if uriStr != "" { 906 uri, err = url.Parse(uriStr) 907 c.Assert(err, check.IsNil) 908 } else { 909 uri = nil 910 } 911 _, err = parseSinkURI(ctx, uri, opts) 912 c.Assert(err, check.NotNil) 913 } 914 } 915 916 func (s MySQLSinkSuite) TestCheckTiDBVariable(c *check.C) { 917 defer testleak.AfterTest(c)() 918 db, mock, err := sqlmock.New() 919 c.Assert(err, check.IsNil) 920 defer db.Close() //nolint:errcheck 921 columns := []string{"Variable_name", "Value"} 922 923 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 924 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 925 ) 926 val, err := checkTiDBVariable(context.TODO(), db, "allow_auto_random_explicit_insert", "1") 927 c.Assert(err, check.IsNil) 928 c.Assert(val, check.Equals, "1") 929 930 mock.ExpectQuery("show session variables like 'no_exist_variable';").WillReturnError(sql.ErrNoRows) 931 val, err = checkTiDBVariable(context.TODO(), db, "no_exist_variable", "0") 932 c.Assert(err, check.IsNil) 933 c.Assert(val, check.Equals, "") 934 935 mock.ExpectQuery("show session variables like 'version';").WillReturnError(sql.ErrConnDone) 936 _, err = checkTiDBVariable(context.TODO(), db, "version", "5.7.25-TiDB-v4.0.0") 937 c.Assert(err, check.ErrorMatches, ".*"+sql.ErrConnDone.Error()) 938 } 939 940 func mockTestDB() (*sql.DB, error) { 941 // mock for test db, which is used querying TiDB session variable 942 db, mock, err := sqlmock.New() 943 if err != nil { 944 return nil, err 945 } 946 columns := []string{"Variable_name", "Value"} 947 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 948 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 949 ) 950 mock.ExpectQuery("show session variables like 'tidb_txn_mode';").WillReturnRows( 951 sqlmock.NewRows(columns).AddRow("tidb_txn_mode", "pessimistic"), 952 ) 953 mock.ExpectClose() 954 return db, nil 955 } 956 957 func (s MySQLSinkSuite) TestAdjustSQLMode(c *check.C) { 958 defer testleak.AfterTest(c)() 959 960 ctx, cancel := context.WithCancel(context.Background()) 961 defer cancel() 962 963 dbIndex := 0 964 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 965 defer func() { 966 dbIndex++ 967 }() 968 if dbIndex == 0 { 969 // test db 970 db, err := mockTestDB() 971 c.Assert(err, check.IsNil) 972 return db, nil 973 } 974 // normal db 975 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 976 c.Assert(err, check.IsNil) 977 mock.ExpectQuery("SELECT @@SESSION.sql_mode;"). 978 WillReturnRows(sqlmock.NewRows([]string{"@@SESSION.sql_mode"}). 979 AddRow("ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE")) 980 mock.ExpectExec("SET sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE';"). 981 WillReturnResult(sqlmock.NewResult(0, 0)) 982 mock.ExpectClose() 983 return db, nil 984 } 985 backupGetDBConn := getDBConnImpl 986 getDBConnImpl = mockGetDBConn 987 defer func() { 988 getDBConnImpl = backupGetDBConn 989 }() 990 991 changefeed := "test-changefeed" 992 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4") 993 c.Assert(err, check.IsNil) 994 rc := config.GetDefaultReplicaConfig() 995 rc.Cyclic = &config.CyclicConfig{ 996 Enable: true, 997 ReplicaID: 1, 998 FilterReplicaID: []uint64{2}, 999 } 1000 f, err := filter.NewFilter(rc) 1001 c.Assert(err, check.IsNil) 1002 cyclicConfig, err := rc.Cyclic.Marshal() 1003 c.Assert(err, check.IsNil) 1004 opts := map[string]string{ 1005 mark.OptCyclicConfig: cyclicConfig, 1006 } 1007 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, opts) 1008 c.Assert(err, check.IsNil) 1009 1010 err = sink.Close(ctx) 1011 c.Assert(err, check.IsNil) 1012 } 1013 1014 type mockUnavailableMySQL struct { 1015 listener net.Listener 1016 quit chan interface{} 1017 wg sync.WaitGroup 1018 } 1019 1020 func newMockUnavailableMySQL(addr string, c *check.C) *mockUnavailableMySQL { 1021 s := &mockUnavailableMySQL{ 1022 quit: make(chan interface{}), 1023 } 1024 l, err := net.Listen("tcp", addr) 1025 c.Assert(err, check.IsNil) 1026 s.listener = l 1027 s.wg.Add(1) 1028 go s.serve(c) 1029 return s 1030 } 1031 1032 func (s *mockUnavailableMySQL) serve(c *check.C) { 1033 defer s.wg.Done() 1034 1035 for { 1036 _, err := s.listener.Accept() 1037 if err != nil { 1038 select { 1039 case <-s.quit: 1040 return 1041 default: 1042 c.Error(err) 1043 } 1044 } else { 1045 s.wg.Add(1) 1046 go func() { 1047 // don't read from TCP connection, to simulate database service unavailable 1048 <-s.quit 1049 s.wg.Done() 1050 }() 1051 } 1052 } 1053 } 1054 1055 func (s *mockUnavailableMySQL) Stop() { 1056 close(s.quit) 1057 s.listener.Close() 1058 s.wg.Wait() 1059 } 1060 1061 func (s MySQLSinkSuite) TestNewMySQLTimeout(c *check.C) { 1062 defer testleak.AfterTest(c)() 1063 1064 addr := "127.0.0.1:33333" 1065 mockMySQL := newMockUnavailableMySQL(addr, c) 1066 defer mockMySQL.Stop() 1067 1068 ctx, cancel := context.WithCancel(context.Background()) 1069 defer cancel() 1070 changefeed := "test-changefeed" 1071 sinkURI, err := url.Parse(fmt.Sprintf("mysql://%s/?read-timeout=2s&timeout=2s", addr)) 1072 c.Assert(err, check.IsNil) 1073 rc := config.GetDefaultReplicaConfig() 1074 f, err := filter.NewFilter(rc) 1075 c.Assert(err, check.IsNil) 1076 _, err = newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1077 c.Assert(errors.Cause(err), check.Equals, driver.ErrBadConn) 1078 } 1079 1080 func (s MySQLSinkSuite) TestNewMySQLSinkExecDML(c *check.C) { 1081 defer testleak.AfterTest(c)() 1082 1083 dbIndex := 0 1084 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1085 defer func() { 1086 dbIndex++ 1087 }() 1088 if dbIndex == 0 { 1089 // test db 1090 db, err := mockTestDB() 1091 c.Assert(err, check.IsNil) 1092 return db, nil 1093 } 1094 // normal db 1095 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1096 c.Assert(err, check.IsNil) 1097 mock.ExpectBegin() 1098 mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`,`b`) VALUES (?,?),(?,?)"). 1099 WithArgs(1, "test", 2, "test"). 1100 WillReturnResult(sqlmock.NewResult(2, 2)) 1101 mock.ExpectCommit() 1102 mock.ExpectBegin() 1103 mock.ExpectExec("REPLACE INTO `s1`.`t2`(`a`,`b`) VALUES (?,?),(?,?)"). 1104 WithArgs(1, "test", 2, "test"). 1105 WillReturnResult(sqlmock.NewResult(2, 2)) 1106 mock.ExpectCommit() 1107 mock.ExpectClose() 1108 return db, nil 1109 } 1110 backupGetDBConn := getDBConnImpl 1111 getDBConnImpl = mockGetDBConn 1112 defer func() { 1113 getDBConnImpl = backupGetDBConn 1114 }() 1115 1116 ctx, cancel := context.WithCancel(context.Background()) 1117 defer cancel() 1118 changefeed := "test-changefeed" 1119 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4") 1120 c.Assert(err, check.IsNil) 1121 rc := config.GetDefaultReplicaConfig() 1122 f, err := filter.NewFilter(rc) 1123 c.Assert(err, check.IsNil) 1124 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1125 c.Assert(err, check.IsNil) 1126 1127 rows := []*model.RowChangedEvent{ 1128 { 1129 StartTs: 1, 1130 CommitTs: 2, 1131 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1132 Columns: []*model.Column{ 1133 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 1134 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 1135 }, 1136 }, 1137 { 1138 StartTs: 1, 1139 CommitTs: 2, 1140 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1141 Columns: []*model.Column{ 1142 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2}, 1143 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 1144 }, 1145 }, 1146 { 1147 StartTs: 5, 1148 CommitTs: 6, 1149 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1150 Columns: []*model.Column{ 1151 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 3}, 1152 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 1153 }, 1154 }, 1155 { 1156 StartTs: 3, 1157 CommitTs: 4, 1158 Table: &model.TableName{Schema: "s1", Table: "t2", TableID: 2}, 1159 Columns: []*model.Column{ 1160 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 1161 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 1162 }, 1163 }, 1164 { 1165 StartTs: 3, 1166 CommitTs: 4, 1167 Table: &model.TableName{Schema: "s1", Table: "t2", TableID: 2}, 1168 Columns: []*model.Column{ 1169 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2}, 1170 {Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"}, 1171 }, 1172 }, 1173 } 1174 1175 err = sink.EmitRowChangedEvents(ctx, rows...) 1176 c.Assert(err, check.IsNil) 1177 1178 err = retry.Do(context.Background(), func() error { 1179 ts, err := sink.FlushRowChangedEvents(ctx, uint64(2)) 1180 c.Assert(err, check.IsNil) 1181 if ts < uint64(2) { 1182 return errors.Errorf("checkpoint ts %d less than resolved ts %d", ts, 2) 1183 } 1184 return nil 1185 }, retry.WithBackoffBaseDelay(20), retry.WithMaxTries(10), retry.WithIsRetryableErr(cerror.IsRetryableError)) 1186 1187 c.Assert(err, check.IsNil) 1188 1189 err = retry.Do(context.Background(), func() error { 1190 ts, err := sink.FlushRowChangedEvents(ctx, uint64(4)) 1191 c.Assert(err, check.IsNil) 1192 if ts < uint64(4) { 1193 return errors.Errorf("checkpoint ts %d less than resolved ts %d", ts, 4) 1194 } 1195 return nil 1196 }, retry.WithBackoffBaseDelay(20), retry.WithMaxTries(10), retry.WithIsRetryableErr(cerror.IsRetryableError)) 1197 c.Assert(err, check.IsNil) 1198 1199 err = sink.Barrier(ctx) 1200 c.Assert(err, check.IsNil) 1201 1202 err = sink.Close(ctx) 1203 c.Assert(err, check.IsNil) 1204 } 1205 1206 func (s MySQLSinkSuite) TestExecDMLRollbackErrDatabaseNotExists(c *check.C) { 1207 defer testleak.AfterTest(c)() 1208 1209 rows := []*model.RowChangedEvent{ 1210 { 1211 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1212 Columns: []*model.Column{ 1213 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 1214 }, 1215 }, 1216 { 1217 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1218 Columns: []*model.Column{ 1219 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2}, 1220 }, 1221 }, 1222 } 1223 1224 errDatabaseNotExists := &dmysql.MySQLError{ 1225 Number: uint16(infoschema.ErrDatabaseNotExists.Code()), 1226 } 1227 1228 dbIndex := 0 1229 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1230 defer func() { 1231 dbIndex++ 1232 }() 1233 if dbIndex == 0 { 1234 // test db 1235 db, err := mockTestDB() 1236 c.Assert(err, check.IsNil) 1237 return db, nil 1238 } 1239 // normal db 1240 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1241 c.Assert(err, check.IsNil) 1242 mock.ExpectBegin() 1243 mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)"). 1244 WithArgs(1, 2). 1245 WillReturnError(errDatabaseNotExists) 1246 mock.ExpectRollback() 1247 mock.ExpectClose() 1248 return db, nil 1249 } 1250 backupGetDBConn := getDBConnImpl 1251 getDBConnImpl = mockGetDBConnErrDatabaseNotExists 1252 defer func() { 1253 getDBConnImpl = backupGetDBConn 1254 }() 1255 1256 ctx, cancel := context.WithCancel(context.Background()) 1257 defer cancel() 1258 changefeed := "test-changefeed" 1259 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1") 1260 c.Assert(err, check.IsNil) 1261 rc := config.GetDefaultReplicaConfig() 1262 f, err := filter.NewFilter(rc) 1263 c.Assert(err, check.IsNil) 1264 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1265 c.Assert(err, check.IsNil) 1266 1267 err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */) 1268 c.Assert(errors.Cause(err), check.Equals, errDatabaseNotExists) 1269 1270 err = sink.Close(ctx) 1271 c.Assert(err, check.IsNil) 1272 } 1273 1274 func (s MySQLSinkSuite) TestExecDMLRollbackErrTableNotExists(c *check.C) { 1275 defer testleak.AfterTest(c)() 1276 1277 rows := []*model.RowChangedEvent{ 1278 { 1279 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1280 Columns: []*model.Column{ 1281 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 1282 }, 1283 }, 1284 { 1285 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1286 Columns: []*model.Column{ 1287 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2}, 1288 }, 1289 }, 1290 } 1291 1292 errTableNotExists := &dmysql.MySQLError{ 1293 Number: uint16(infoschema.ErrTableNotExists.Code()), 1294 } 1295 1296 dbIndex := 0 1297 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1298 defer func() { 1299 dbIndex++ 1300 }() 1301 if dbIndex == 0 { 1302 // test db 1303 db, err := mockTestDB() 1304 c.Assert(err, check.IsNil) 1305 return db, nil 1306 } 1307 // normal db 1308 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1309 c.Assert(err, check.IsNil) 1310 mock.ExpectBegin() 1311 mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)"). 1312 WithArgs(1, 2). 1313 WillReturnError(errTableNotExists) 1314 mock.ExpectRollback() 1315 mock.ExpectClose() 1316 return db, nil 1317 } 1318 backupGetDBConn := getDBConnImpl 1319 getDBConnImpl = mockGetDBConnErrDatabaseNotExists 1320 defer func() { 1321 getDBConnImpl = backupGetDBConn 1322 }() 1323 1324 ctx, cancel := context.WithCancel(context.Background()) 1325 defer cancel() 1326 changefeed := "test-changefeed" 1327 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1") 1328 c.Assert(err, check.IsNil) 1329 rc := config.GetDefaultReplicaConfig() 1330 f, err := filter.NewFilter(rc) 1331 c.Assert(err, check.IsNil) 1332 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1333 c.Assert(err, check.IsNil) 1334 1335 err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */) 1336 c.Assert(errors.Cause(err), check.Equals, errTableNotExists) 1337 1338 err = sink.Close(ctx) 1339 c.Assert(err, check.IsNil) 1340 } 1341 1342 func (s MySQLSinkSuite) TestExecDMLRollbackErrRetryable(c *check.C) { 1343 defer testleak.AfterTest(c)() 1344 1345 rows := []*model.RowChangedEvent{ 1346 { 1347 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1348 Columns: []*model.Column{ 1349 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1}, 1350 }, 1351 }, 1352 { 1353 Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1}, 1354 Columns: []*model.Column{ 1355 {Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2}, 1356 }, 1357 }, 1358 } 1359 1360 errLockDeadlock := &dmysql.MySQLError{ 1361 Number: mysql.ErrLockDeadlock, 1362 } 1363 1364 dbIndex := 0 1365 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1366 defer func() { 1367 dbIndex++ 1368 }() 1369 if dbIndex == 0 { 1370 // test db 1371 db, err := mockTestDB() 1372 c.Assert(err, check.IsNil) 1373 return db, nil 1374 } 1375 // normal db 1376 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1377 c.Assert(err, check.IsNil) 1378 for i := 0; i < defaultDMLMaxRetryTime; i++ { 1379 mock.ExpectBegin() 1380 mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)"). 1381 WithArgs(1, 2). 1382 WillReturnError(errLockDeadlock) 1383 mock.ExpectRollback() 1384 } 1385 mock.ExpectClose() 1386 return db, nil 1387 } 1388 backupGetDBConn := getDBConnImpl 1389 getDBConnImpl = mockGetDBConnErrDatabaseNotExists 1390 defer func() { 1391 getDBConnImpl = backupGetDBConn 1392 }() 1393 1394 ctx, cancel := context.WithCancel(context.Background()) 1395 defer cancel() 1396 changefeed := "test-changefeed" 1397 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1") 1398 c.Assert(err, check.IsNil) 1399 rc := config.GetDefaultReplicaConfig() 1400 f, err := filter.NewFilter(rc) 1401 c.Assert(err, check.IsNil) 1402 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1403 c.Assert(err, check.IsNil) 1404 1405 err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */) 1406 c.Assert(errors.Cause(err), check.Equals, errLockDeadlock) 1407 1408 err = sink.Close(ctx) 1409 c.Assert(err, check.IsNil) 1410 } 1411 1412 func (s MySQLSinkSuite) TestNewMySQLSinkExecDDL(c *check.C) { 1413 defer testleak.AfterTest(c)() 1414 1415 dbIndex := 0 1416 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1417 defer func() { 1418 dbIndex++ 1419 }() 1420 if dbIndex == 0 { 1421 // test db 1422 db, err := mockTestDB() 1423 c.Assert(err, check.IsNil) 1424 return db, nil 1425 } 1426 // normal db 1427 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1428 c.Assert(err, check.IsNil) 1429 mock.ExpectBegin() 1430 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 1431 mock.ExpectExec("ALTER TABLE test.t1 ADD COLUMN a int").WillReturnResult(sqlmock.NewResult(1, 1)) 1432 mock.ExpectCommit() 1433 mock.ExpectBegin() 1434 mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1)) 1435 mock.ExpectExec("ALTER TABLE test.t1 ADD COLUMN a int"). 1436 WillReturnError(&dmysql.MySQLError{ 1437 Number: uint16(infoschema.ErrColumnExists.Code()), 1438 }) 1439 mock.ExpectRollback() 1440 mock.ExpectClose() 1441 return db, nil 1442 } 1443 backupGetDBConn := getDBConnImpl 1444 getDBConnImpl = mockGetDBConn 1445 defer func() { 1446 getDBConnImpl = backupGetDBConn 1447 }() 1448 1449 ctx, cancel := context.WithCancel(context.Background()) 1450 defer cancel() 1451 changefeed := "test-changefeed" 1452 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4") 1453 c.Assert(err, check.IsNil) 1454 rc := config.GetDefaultReplicaConfig() 1455 rc.Filter = &config.FilterConfig{ 1456 Rules: []string{"test.t1"}, 1457 } 1458 f, err := filter.NewFilter(rc) 1459 c.Assert(err, check.IsNil) 1460 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1461 c.Assert(err, check.IsNil) 1462 1463 ddl1 := &model.DDLEvent{ 1464 StartTs: 1000, 1465 CommitTs: 1010, 1466 TableInfo: &model.SimpleTableInfo{ 1467 Schema: "test", 1468 Table: "t1", 1469 }, 1470 Type: timodel.ActionAddColumn, 1471 Query: "ALTER TABLE test.t1 ADD COLUMN a int", 1472 } 1473 ddl2 := &model.DDLEvent{ 1474 StartTs: 1020, 1475 CommitTs: 1030, 1476 TableInfo: &model.SimpleTableInfo{ 1477 Schema: "test", 1478 Table: "t2", 1479 }, 1480 Type: timodel.ActionAddColumn, 1481 Query: "ALTER TABLE test.t1 ADD COLUMN a int", 1482 } 1483 1484 err = sink.EmitDDLEvent(ctx, ddl1) 1485 c.Assert(err, check.IsNil) 1486 err = sink.EmitDDLEvent(ctx, ddl2) 1487 c.Assert(cerror.ErrDDLEventIgnored.Equal(err), check.IsTrue) 1488 // DDL execute failed, but error can be ignored 1489 err = sink.EmitDDLEvent(ctx, ddl1) 1490 c.Assert(err, check.IsNil) 1491 1492 err = sink.Close(ctx) 1493 c.Assert(err, check.IsNil) 1494 } 1495 1496 func (s MySQLSinkSuite) TestNewMySQLSink(c *check.C) { 1497 defer testleak.AfterTest(c)() 1498 1499 dbIndex := 0 1500 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 1501 defer func() { 1502 dbIndex++ 1503 }() 1504 if dbIndex == 0 { 1505 // test db 1506 db, err := mockTestDB() 1507 c.Assert(err, check.IsNil) 1508 return db, nil 1509 } 1510 // normal db 1511 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 1512 mock.ExpectClose() 1513 c.Assert(err, check.IsNil) 1514 return db, nil 1515 } 1516 backupGetDBConn := getDBConnImpl 1517 getDBConnImpl = mockGetDBConn 1518 defer func() { 1519 getDBConnImpl = backupGetDBConn 1520 }() 1521 1522 ctx, cancel := context.WithCancel(context.Background()) 1523 defer cancel() 1524 changefeed := "test-changefeed" 1525 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4") 1526 c.Assert(err, check.IsNil) 1527 rc := config.GetDefaultReplicaConfig() 1528 f, err := filter.NewFilter(rc) 1529 c.Assert(err, check.IsNil) 1530 sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{}) 1531 c.Assert(err, check.IsNil) 1532 err = sink.Close(ctx) 1533 c.Assert(err, check.IsNil) 1534 }