github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/syncer/data_validator_test.go (about) 1 // Copyright 2022 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 syncer 15 16 import ( 17 "context" 18 "crypto/sha256" 19 "encoding/hex" 20 "testing" 21 "time" 22 23 "github.com/DATA-DOG/go-sqlmock" 24 "github.com/go-mysql-org/go-mysql/mysql" 25 "github.com/go-mysql-org/go-mysql/replication" 26 "github.com/pingcap/errors" 27 "github.com/pingcap/failpoint" 28 "github.com/pingcap/tidb/pkg/util/filter" 29 regexprrouter "github.com/pingcap/tidb/pkg/util/regexpr-router" 30 router "github.com/pingcap/tidb/pkg/util/table-router" 31 "github.com/pingcap/tiflow/dm/config" 32 "github.com/pingcap/tiflow/dm/config/dbconfig" 33 "github.com/pingcap/tiflow/dm/pb" 34 "github.com/pingcap/tiflow/dm/pkg/binlog" 35 "github.com/pingcap/tiflow/dm/pkg/binlog/event" 36 "github.com/pingcap/tiflow/dm/pkg/conn" 37 tcontext "github.com/pingcap/tiflow/dm/pkg/context" 38 "github.com/pingcap/tiflow/dm/pkg/gtid" 39 "github.com/pingcap/tiflow/dm/pkg/log" 40 "github.com/pingcap/tiflow/dm/pkg/retry" 41 "github.com/pingcap/tiflow/dm/pkg/schema" 42 "github.com/pingcap/tiflow/dm/pkg/utils" 43 "github.com/pingcap/tiflow/dm/syncer/binlogstream" 44 "github.com/pingcap/tiflow/dm/syncer/dbconn" 45 "github.com/stretchr/testify/require" 46 ) 47 48 func genEventGenerator(t *testing.T) *event.Generator { 49 t.Helper() 50 previousGTIDSetStr := "3ccc475b-2343-11e7-be21-6c0b84d59f30:1-14" 51 previousGTIDSet, err := gtid.ParserGTID(mysql.MySQLFlavor, previousGTIDSetStr) 52 require.NoError(t, err) 53 latestGTIDStr := "3ccc475b-2343-11e7-be21-6c0b84d59f30:14" 54 latestGTID, err := gtid.ParserGTID(mysql.MySQLFlavor, latestGTIDStr) 55 require.NoError(t, err) 56 eventsGenerator, err := event.NewGenerator(mysql.MySQLFlavor, 1, 0, latestGTID, previousGTIDSet, 0) 57 require.NoError(t, err) 58 require.NoError(t, err) 59 60 return eventsGenerator 61 } 62 63 func genSubtaskConfig(t *testing.T) *config.SubTaskConfig { 64 t.Helper() 65 loaderCfg := config.LoaderConfig{ 66 Dir: t.TempDir(), 67 } 68 cfg := &config.SubTaskConfig{ 69 From: config.GetDBConfigForTest(), 70 To: config.GetDBConfigForTest(), 71 Timezone: "UTC", 72 ServerID: 101, 73 Name: "validator_ut", 74 ShadowTableRules: []string{config.DefaultShadowTableRules}, 75 TrashTableRules: []string{config.DefaultTrashTableRules}, 76 Mode: config.ModeIncrement, 77 Flavor: mysql.MySQLFlavor, 78 LoaderConfig: loaderCfg, 79 SyncerConfig: config.SyncerConfig{ 80 EnableGTID: false, 81 }, 82 ValidatorCfg: config.ValidatorConfig{ 83 Mode: config.ModeFull, 84 WorkerCount: 1, 85 }, 86 } 87 cfg.Experimental.AsyncCheckpointFlush = true 88 cfg.From.Adjust() 89 cfg.To.Adjust() 90 require.NoError(t, cfg.ValidatorCfg.Adjust()) 91 92 cfg.UseRelay = false 93 94 return cfg 95 } 96 97 func TestValidatorStartStopAndInitialize(t *testing.T) { 98 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 99 defer func() { 100 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 101 }() 102 cfg := genSubtaskConfig(t) 103 syncerObj := NewSyncer(cfg, nil, nil) 104 105 // validator already running 106 validator := NewContinuousDataValidator(cfg, syncerObj, false) 107 validator.stage = pb.Stage_Running 108 validator.Start(pb.Stage_InvalidStage) 109 // if validator already running, Start will return immediately, so we check validator.ctx which has not initialized. 110 require.Nil(t, validator.ctx) 111 112 // failed to init 113 cfg.From = dbconfig.DBConfig{ 114 Host: "invalid host", 115 Port: 3306, 116 User: "root", 117 } 118 validator = NewContinuousDataValidator(cfg, syncerObj, false) 119 err := validator.initialize() 120 require.Equal(t, pb.Stage_Stopped, validator.Stage()) 121 require.Error(t, err) 122 123 // init using mocked db 124 _, _, err = conn.InitMockDBFull() 125 require.NoError(t, err) 126 defer func() { 127 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 128 }() 129 validator = NewContinuousDataValidator(cfg, syncerObj, false) 130 validator.persistHelper.schemaInitialized.Store(true) 131 err = validator.initialize() 132 require.NoError(t, err) 133 134 // normal start & stop 135 validator = NewContinuousDataValidator(cfg, syncerObj, false) 136 validator.persistHelper.schemaInitialized.Store(true) 137 validator.Start(pb.Stage_Running) 138 defer validator.Stop() // in case assert failed before Stop 139 require.Equal(t, pb.Stage_Running, validator.Stage()) 140 require.True(t, validator.Started()) 141 validator.Stop() 142 require.Equal(t, pb.Stage_Stopped, validator.Stage()) 143 144 // stop before start, should not panic 145 validator = NewContinuousDataValidator(cfg, syncerObj, false) 146 validator.persistHelper.schemaInitialized.Store(true) 147 validator.Stop() 148 } 149 150 func TestValidatorFillResult(t *testing.T) { 151 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 152 defer func() { 153 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 154 }() 155 cfg := genSubtaskConfig(t) 156 syncerObj := NewSyncer(cfg, nil, nil) 157 _, _, err := conn.InitMockDBFull() 158 require.NoError(t, err) 159 defer func() { 160 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 161 }() 162 163 validator := NewContinuousDataValidator(cfg, syncerObj, false) 164 validator.persistHelper.schemaInitialized.Store(true) 165 validator.Start(pb.Stage_Running) 166 defer validator.Stop() // in case assert failed before Stop 167 validator.fillResult(errors.New("test error")) 168 require.Len(t, validator.result.Errors, 1) 169 validator.fillResult(errors.New("test error")) 170 require.Len(t, validator.result.Errors, 2) 171 validator.Stop() 172 validator.fillResult(validator.ctx.Err()) 173 require.Len(t, validator.result.Errors, 2) 174 } 175 176 func TestValidatorErrorProcessRoutine(t *testing.T) { 177 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 178 defer func() { 179 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 180 }() 181 cfg := genSubtaskConfig(t) 182 syncerObj := NewSyncer(cfg, nil, nil) 183 _, _, err := conn.InitMockDBFull() 184 require.NoError(t, err) 185 defer func() { 186 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 187 }() 188 189 validator := NewContinuousDataValidator(cfg, syncerObj, false) 190 validator.persistHelper.schemaInitialized.Store(true) 191 validator.Start(pb.Stage_Running) 192 defer validator.Stop() 193 require.Equal(t, pb.Stage_Running, validator.Stage()) 194 validator.sendError(errors.New("test error")) 195 require.True(t, utils.WaitSomething(20, 100*time.Millisecond, func() bool { 196 return validator.Stage() == pb.Stage_Stopped 197 })) 198 require.Len(t, validator.result.Errors, 1) 199 } 200 201 func TestValidatorDeadLock(t *testing.T) { 202 require.NoError(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 203 defer func() { 204 require.NoError(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 205 }() 206 cfg := genSubtaskConfig(t) 207 syncerObj := NewSyncer(cfg, nil, nil) 208 _, _, err := conn.InitMockDBFull() 209 require.NoError(t, err) 210 defer func() { 211 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 212 }() 213 214 validator := NewContinuousDataValidator(cfg, syncerObj, false) 215 validator.persistHelper.schemaInitialized.Store(true) 216 validator.Start(pb.Stage_Running) 217 require.Equal(t, pb.Stage_Running, validator.Stage()) 218 validator.wg.Add(1) 219 go func() { 220 defer func() { 221 // ignore panic when try to insert error to a closed channel, 222 // which will happen after the validator is successfully stopped. 223 // The panic is expected. 224 validator.wg.Done() 225 // nolint:errcheck 226 recover() 227 }() 228 for i := 0; i < 100; i++ { 229 validator.sendError(context.Canceled) // prevent from stopping the validator 230 } 231 }() 232 // stuck if the validator doesn't unlock before waiting wg 233 validator.Stop() 234 require.Equal(t, pb.Stage_Stopped, validator.Stage()) 235 } 236 237 type mockedCheckPointForValidator struct { 238 CheckPoint 239 cnt int 240 currLoc binlog.Location 241 nextLoc binlog.Location 242 } 243 244 func (c *mockedCheckPointForValidator) FlushedGlobalPoint() binlog.Location { 245 c.cnt++ 246 if c.cnt <= 2 { 247 return c.currLoc 248 } 249 return c.nextLoc 250 } 251 252 func TestValidatorWaitSyncerSynced(t *testing.T) { 253 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 254 defer func() { 255 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 256 }() 257 cfg := genSubtaskConfig(t) 258 syncerObj := NewSyncer(cfg, nil, nil) 259 _, _, err := conn.InitMockDBFull() 260 require.NoError(t, err) 261 defer func() { 262 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 263 }() 264 265 currLoc := binlog.MustZeroLocation(cfg.Flavor) 266 validator := NewContinuousDataValidator(cfg, syncerObj, false) 267 validator.persistHelper.schemaInitialized.Store(true) 268 require.NoError(t, validator.initialize()) 269 require.NoError(t, validator.waitSyncerSynced(currLoc)) 270 271 // cancelled 272 currLoc.Position = mysql.Position{ 273 Name: "mysql-bin.000001", 274 Pos: 100, 275 } 276 validator = NewContinuousDataValidator(cfg, syncerObj, false) 277 validator.persistHelper.schemaInitialized.Store(true) 278 require.NoError(t, validator.initialize()) 279 validator.cancel() 280 require.ErrorIs(t, validator.waitSyncerSynced(currLoc), context.Canceled) 281 282 currLoc.Position = mysql.Position{ 283 Name: "mysql-bin.000001", 284 Pos: 100, 285 } 286 syncerObj.checkpoint = &mockedCheckPointForValidator{ 287 currLoc: binlog.MustZeroLocation(cfg.Flavor), 288 nextLoc: currLoc, 289 } 290 validator = NewContinuousDataValidator(cfg, syncerObj, false) 291 validator.persistHelper.schemaInitialized.Store(true) 292 require.NoError(t, validator.initialize()) 293 require.NoError(t, validator.waitSyncerSynced(currLoc)) 294 } 295 296 func TestValidatorWaitSyncerRunning(t *testing.T) { 297 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 298 defer func() { 299 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 300 }() 301 cfg := genSubtaskConfig(t) 302 syncerObj := NewSyncer(cfg, nil, nil) 303 _, _, err := conn.InitMockDBFull() 304 require.NoError(t, err) 305 defer func() { 306 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 307 }() 308 309 validator := NewContinuousDataValidator(cfg, syncerObj, false) 310 validator.persistHelper.schemaInitialized.Store(true) 311 require.NoError(t, validator.initialize()) 312 validator.cancel() 313 require.Error(t, validator.waitSyncerRunning()) 314 315 validator = NewContinuousDataValidator(cfg, syncerObj, false) 316 validator.persistHelper.schemaInitialized.Store(true) 317 require.NoError(t, validator.initialize()) 318 syncerObj.running.Store(true) 319 require.NoError(t, validator.waitSyncerRunning()) 320 321 validator = NewContinuousDataValidator(cfg, syncerObj, false) 322 validator.persistHelper.schemaInitialized.Store(true) 323 require.NoError(t, validator.initialize()) 324 syncerObj.running.Store(false) 325 go func() { 326 time.Sleep(3 * time.Second) 327 syncerObj.running.Store(true) 328 }() 329 require.NoError(t, validator.waitSyncerRunning()) 330 } 331 332 func TestValidatorDoValidate(t *testing.T) { 333 var ( 334 schemaName = "test" 335 tableName = "tbl" 336 tableName2 = "tbl2" 337 tableName3 = "tbl3" 338 tableName4 = "tbl4" 339 createTableSQL = "CREATE TABLE `" + tableName + "`(id int primary key, v varchar(100))" 340 createTableSQL2 = "CREATE TABLE `" + tableName2 + "`(id int primary key)" 341 createTableSQL3 = "CREATE TABLE `" + tableName3 + "`(id int, v varchar(100))" 342 tableNameInfo = filter.Table{Schema: schemaName, Name: tableName} 343 tableNameInfo2 = filter.Table{Schema: schemaName, Name: tableName2} 344 tableNameInfo3 = filter.Table{Schema: schemaName, Name: tableName3} 345 ) 346 createAST1, err := parseSQL(createTableSQL) 347 require.NoError(t, err) 348 createAST2, err := parseSQL(createTableSQL2) 349 require.NoError(t, err) 350 createAST3, err := parseSQL(createTableSQL3) 351 require.NoError(t, err) 352 353 cfg := genSubtaskConfig(t) 354 _, dbMock, err := conn.InitMockDBFull() 355 require.NoError(t, err) 356 defer func() { 357 conn.DefaultDBProvider = &conn.DefaultDBProviderImpl{} 358 }() 359 dbMock.ExpectQuery("select .* from .*_validator_checkpoint.*").WillReturnRows( 360 dbMock.NewRows([]string{"", "", "", "", "", "", ""}).AddRow("mysql-bin.000001", 100, "", 0, 0, 0, 1)) 361 dbMock.ExpectQuery("select .* from .*_validator_pending_change.*").WillReturnRows( 362 dbMock.NewRows([]string{"", "", "", "", ""}).AddRow(schemaName, tableName, "11", 363 // insert with pk=11 364 "{\"key\": \"11\", \"data\": [\"11\", \"a\"], \"tp\": 0, \"first-validate-ts\": 0, \"failed-cnt\": 0}", 1)) 365 dbMock.ExpectQuery("select .* from .*_validator_table_status.*").WillReturnRows( 366 dbMock.NewRows([]string{"", "", "", "", "", ""}).AddRow(schemaName, tableName4, schemaName, tableName4, pb.Stage_Stopped, "load from meta")) 367 368 syncerObj := NewSyncer(cfg, nil, nil) 369 syncerObj.running.Store(true) 370 syncerObj.tableRouter, err = regexprrouter.NewRegExprRouter(cfg.CaseSensitive, []*router.TableRule{}) 371 require.NoError(t, err) 372 currLoc := binlog.MustZeroLocation(cfg.Flavor) 373 currLoc.Position = mysql.Position{ 374 Name: "mysql-bin.000001", 375 Pos: 3000, 376 } 377 syncerObj.checkpoint = &mockedCheckPointForValidator{ 378 currLoc: binlog.MustZeroLocation(cfg.Flavor), 379 nextLoc: currLoc, 380 cnt: 2, 381 } 382 db, mock, err := sqlmock.New() 383 require.NoError(t, err) 384 mock.MatchExpectationsInOrder(false) 385 mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows( 386 mock.NewRows([]string{"Variable_name", "Value"}).AddRow("sql_mode", ""), 387 ) 388 mock.ExpectBegin() 389 mock.ExpectExec("SET SESSION SQL_MODE.*").WillReturnResult(sqlmock.NewResult(1, 1)) 390 mock.ExpectCommit() 391 mock.ExpectQuery("SHOW CREATE TABLE " + tableNameInfo.String() + ".*").WillReturnRows( 392 mock.NewRows([]string{"Table", "Create Table"}).AddRow(tableName, createTableSQL), 393 ) 394 mock.ExpectQuery("SHOW CREATE TABLE " + tableNameInfo2.String() + ".*").WillReturnRows( 395 mock.NewRows([]string{"Table", "Create Table"}).AddRow(tableName2, createTableSQL2), 396 ) 397 mock.ExpectQuery("SHOW CREATE TABLE " + tableNameInfo3.String() + ".*").WillReturnRows( 398 mock.NewRows([]string{"Table", "Create Table"}).AddRow(tableName3, createTableSQL3), 399 ) 400 dbConn, err := db.Conn(context.Background()) 401 require.NoError(t, err) 402 syncerObj.downstreamTrackConn = dbconn.NewDBConn(cfg, conn.NewBaseConnForTest(dbConn, &retry.FiniteRetryStrategy{})) 403 syncerObj.schemaTracker, err = schema.NewTestTracker(context.Background(), cfg.Name, syncerObj.downstreamTrackConn, log.L()) 404 defer syncerObj.schemaTracker.Close() 405 require.NoError(t, err) 406 require.NoError(t, syncerObj.schemaTracker.CreateSchemaIfNotExists(schemaName)) 407 require.NoError(t, syncerObj.schemaTracker.Exec(context.Background(), schemaName, createAST1)) 408 require.NoError(t, syncerObj.schemaTracker.Exec(context.Background(), schemaName, createAST2)) 409 require.NoError(t, syncerObj.schemaTracker.Exec(context.Background(), schemaName, createAST3)) 410 411 generator := genEventGenerator(t) 412 rotateEvent, _, err := generator.Rotate("mysql-bin.000001", 0) 413 require.NoError(t, err) 414 insertData := []*event.DMLData{ 415 { 416 TableID: 11, 417 Schema: schemaName, 418 Table: tableName, 419 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 420 Rows: [][]interface{}{ 421 {int32(1), "a"}, 422 {int32(2), "b"}, 423 {int32(3), "c"}, 424 }, 425 }, 426 // 2 columns in binlog, but ddl of tbl2 only has one column 427 { 428 TableID: 12, 429 Schema: schemaName, 430 Table: tableName2, 431 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 432 Rows: [][]interface{}{ 433 {int32(1), "a"}, 434 {int32(2), "b"}, 435 {int32(3), "c"}, 436 }, 437 }, 438 // tbl3 has no primary key 439 { 440 TableID: 13, 441 Schema: schemaName, 442 Table: tableName3, 443 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 444 Rows: [][]interface{}{ 445 {int32(1), "a"}, 446 {int32(2), "b"}, 447 {int32(3), "c"}, 448 }, 449 }, 450 // tbl3 has no primary key, since we met it before, will return immediately 451 { 452 TableID: 13, 453 Schema: schemaName, 454 Table: tableName3, 455 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 456 Rows: [][]interface{}{ 457 {int32(4), "a"}, 458 }, 459 }, 460 } 461 updateData := []*event.DMLData{ 462 { 463 TableID: 11, 464 Schema: schemaName, 465 Table: tableName, 466 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 467 Rows: [][]interface{}{ 468 // update non-primary column 469 {int32(3), "c"}, 470 {int32(3), "d"}, 471 // update primary column and non-primary column 472 {int32(1), "a"}, 473 {int32(4), "b"}, 474 }, 475 }, 476 } 477 deleteData := []*event.DMLData{ 478 { 479 TableID: 11, 480 Schema: schemaName, 481 Table: tableName, 482 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 483 Rows: [][]interface{}{ 484 {int32(3), "c"}, 485 }, 486 }, 487 // no ddl for this table 488 { 489 TableID: 14, 490 Schema: schemaName, 491 Table: tableName4, 492 ColumnType: []byte{mysql.MYSQL_TYPE_LONG, mysql.MYSQL_TYPE_STRING}, 493 Rows: [][]interface{}{ 494 {int32(4), "c"}, 495 }, 496 }, 497 } 498 dmlEvents, _, err := generator.GenDMLEvents(replication.WRITE_ROWS_EVENTv2, insertData, 0) 499 require.NoError(t, err) 500 updateEvents, _, err := generator.GenDMLEvents(replication.UPDATE_ROWS_EVENTv2, updateData, 0) 501 require.NoError(t, err) 502 deleteEvents, _, err := generator.GenDMLEvents(replication.DELETE_ROWS_EVENTv2, deleteData, 0) 503 require.NoError(t, err) 504 allEvents := []*replication.BinlogEvent{rotateEvent} 505 allEvents = append(allEvents, dmlEvents...) 506 allEvents = append(allEvents, updateEvents...) 507 allEvents = append(allEvents, deleteEvents...) 508 mockStreamerProducer := &MockStreamProducer{events: allEvents} 509 mockStreamer, err := mockStreamerProducer.GenerateStreamFrom(binlog.MustZeroLocation(mysql.MySQLFlavor)) 510 require.NoError(t, err) 511 512 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ", `return()`)) 513 defer func() { 514 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/ValidatorMockUpstreamTZ")) 515 }() 516 validator := NewContinuousDataValidator(cfg, syncerObj, false) 517 validator.validateInterval = 10 * time.Minute // we don't want worker start validate 518 validator.persistHelper.schemaInitialized.Store(true) 519 require.NoError(t, validator.initialize()) 520 validator.streamerController = binlogstream.NewStreamerController4Test( 521 mockStreamerProducer, 522 mockStreamer, 523 ) 524 validator.wg.Add(1) // wg.Done is run in doValidate 525 validator.doValidate() 526 validator.Stop() 527 // 3 real insert, 1 transformed from an update(updating key) 528 require.Equal(t, int64(4), validator.processedRowCounts[rowInsert].Load()) 529 require.Equal(t, int64(1), validator.processedRowCounts[rowUpdated].Load()) 530 // 1 real delete, 1 transformed from an update(updating key) 531 require.Equal(t, int64(2), validator.processedRowCounts[rowDeleted].Load()) 532 533 require.NotNil(t, validator.location) 534 require.Equal(t, mysql.Position{Name: "mysql-bin.000001", Pos: 100}, validator.location.Position) 535 require.Equal(t, "", validator.location.GTIDSetStr()) 536 require.Len(t, validator.loadedPendingChanges, 1) 537 ft := filter.Table{Schema: schemaName, Name: tableName} 538 require.Contains(t, validator.loadedPendingChanges, ft.String()) 539 require.Len(t, validator.loadedPendingChanges[ft.String()].jobs, 1) 540 require.Contains(t, validator.loadedPendingChanges[ft.String()].jobs, "11") 541 require.Equal(t, validator.loadedPendingChanges[ft.String()].jobs["11"].Tp, rowInsert) 542 require.Len(t, validator.tableStatus, 4) 543 require.Contains(t, validator.tableStatus, ft.String()) 544 require.Equal(t, pb.Stage_Running, validator.tableStatus[ft.String()].stage) 545 require.Zero(t, validator.newErrorRowCount.Load()) 546 547 ft = filter.Table{Schema: schemaName, Name: tableName2} 548 require.Contains(t, validator.tableStatus, ft.String()) 549 require.Equal(t, pb.Stage_Stopped, validator.tableStatus[ft.String()].stage) 550 require.Equal(t, moreColumnInBinlogMsg, validator.tableStatus[ft.String()].message) 551 ft = filter.Table{Schema: schemaName, Name: tableName3} 552 require.Contains(t, validator.tableStatus, ft.String()) 553 require.Equal(t, pb.Stage_Stopped, validator.tableStatus[ft.String()].stage) 554 require.Equal(t, tableWithoutPrimaryKeyMsg, validator.tableStatus[ft.String()].message) 555 // this one is loaded from meta data 556 ft = filter.Table{Schema: schemaName, Name: tableName4} 557 require.Contains(t, validator.tableStatus, ft.String()) 558 require.Equal(t, pb.Stage_Stopped, validator.tableStatus[ft.String()].stage) 559 require.Equal(t, "load from meta", validator.tableStatus[ft.String()].message) 560 } 561 562 func TestValidatorGetRowChangeType(t *testing.T) { 563 require.Equal(t, rowInsert, getRowChangeType(replication.WRITE_ROWS_EVENTv0)) 564 require.Equal(t, rowInsert, getRowChangeType(replication.WRITE_ROWS_EVENTv1)) 565 require.Equal(t, rowInsert, getRowChangeType(replication.WRITE_ROWS_EVENTv2)) 566 require.Equal(t, rowUpdated, getRowChangeType(replication.UPDATE_ROWS_EVENTv0)) 567 require.Equal(t, rowUpdated, getRowChangeType(replication.UPDATE_ROWS_EVENTv1)) 568 require.Equal(t, rowUpdated, getRowChangeType(replication.UPDATE_ROWS_EVENTv2)) 569 require.Equal(t, rowDeleted, getRowChangeType(replication.DELETE_ROWS_EVENTv0)) 570 require.Equal(t, rowDeleted, getRowChangeType(replication.DELETE_ROWS_EVENTv1)) 571 require.Equal(t, rowDeleted, getRowChangeType(replication.DELETE_ROWS_EVENTv2)) 572 } 573 574 func TestValidatorGenRowKey(t *testing.T) { 575 require.Equal(t, "a", genRowKeyByString([]string{"a"})) 576 require.Equal(t, "a\tb", genRowKeyByString([]string{"a", "b"})) 577 require.Equal(t, "a\tb\tc", genRowKeyByString([]string{"a", "b", "c"})) 578 var bytes []byte 579 for i := 0; i < 100; i++ { 580 bytes = append(bytes, 'a') 581 } 582 { 583 longStr := string(bytes[:maxRowKeyLength]) 584 require.Equal(t, longStr, genRowKeyByString([]string{longStr})) 585 } 586 { 587 longStr := string(bytes[:maxRowKeyLength+1]) 588 sum := sha256.Sum256([]byte(longStr)) 589 sha := hex.EncodeToString(sum[:]) 590 require.Equal(t, sha, genRowKeyByString([]string{longStr})) 591 } 592 } 593 594 func TestValidatorGetValidationStatus(t *testing.T) { 595 cfg := genSubtaskConfig(t) 596 syncerObj := NewSyncer(cfg, nil, nil) 597 validator := NewContinuousDataValidator(cfg, syncerObj, false) 598 expected := map[string]*pb.ValidationTableStatus{ 599 "`db`.`tbl1`": { 600 SrcTable: "`db`.`tbl1`", 601 DstTable: "`db`.`tbl1`", 602 Stage: pb.Stage_Running, 603 Message: "", 604 }, 605 "`db`.`tbl2`": { 606 SrcTable: "`db`.`tbl2`", 607 DstTable: "`db`.`tbl2`", 608 Stage: pb.Stage_Stopped, 609 Message: tableWithoutPrimaryKeyMsg, 610 }, 611 } 612 validator.tableStatus = map[string]*tableValidateStatus{ 613 "`db`.`tbl1`": { 614 source: filter.Table{Schema: "db", Name: "tbl1"}, 615 target: filter.Table{Schema: "db", Name: "tbl1"}, 616 stage: pb.Stage_Running, 617 }, 618 "`db`.`tbl2`": { 619 source: filter.Table{Schema: "db", Name: "tbl2"}, 620 target: filter.Table{Schema: "db", Name: "tbl2"}, 621 stage: pb.Stage_Stopped, 622 message: tableWithoutPrimaryKeyMsg, 623 }, 624 } 625 ret := validator.GetValidatorTableStatus(pb.Stage_InvalidStage) 626 require.Equal(t, len(expected), len(ret)) 627 for _, result := range ret { 628 ent, ok := expected[result.SrcTable] 629 require.Equal(t, ok, true) 630 require.EqualValues(t, ent, result) 631 } 632 ret = validator.GetValidatorTableStatus(pb.Stage_Running) 633 require.Equal(t, 1, len(ret)) 634 for _, result := range ret { 635 ent, ok := expected[result.SrcTable] 636 require.Equal(t, ok, true) 637 require.EqualValues(t, ent, result) 638 } 639 ret = validator.GetValidatorTableStatus(pb.Stage_Stopped) 640 require.Equal(t, 1, len(ret)) 641 for _, result := range ret { 642 ent, ok := expected[result.SrcTable] 643 require.Equal(t, ok, true) 644 require.EqualValues(t, ent, result) 645 } 646 } 647 648 func TestValidatorGetValidationError(t *testing.T) { 649 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/MockValidationQuery", `return(true)`)) 650 defer func() { 651 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/MockValidationQuery")) 652 }() 653 db, dbMock, err := sqlmock.New() 654 require.Equal(t, log.InitLogger(&log.Config{}), nil) 655 require.NoError(t, err) 656 cfg := genSubtaskConfig(t) 657 syncerObj := NewSyncer(cfg, nil, nil) 658 validator := NewContinuousDataValidator(cfg, syncerObj, false) 659 validator.ctx, validator.cancel = context.WithCancel(context.Background()) 660 validator.tctx = tcontext.NewContext(validator.ctx, validator.L) 661 // all error 662 dbMock.ExpectQuery("SELECT .* FROM " + validator.persistHelper.errorChangeTableName + " WHERE source=?").WithArgs(validator.cfg.SourceID).WillReturnRows( 663 sqlmock.NewRows([]string{"id", "source", "src_schema_name", "src_table_name", "dst_schema_name", "dst_table_name", "data", "dst_data", "error_type", "status", "update_time"}).AddRow( 664 1, "mysql-replica", "srcdb", "srctbl", "dstdb", "dsttbl", "source data", "unexpected data", 2, 1, "2022-03-01", 665 ), 666 ) 667 // filter by status 668 dbMock.ExpectQuery("SELECT .* FROM "+validator.persistHelper.errorChangeTableName+" WHERE source = \\? AND status=\\?"). 669 WithArgs(validator.cfg.SourceID, int(pb.ValidateErrorState_IgnoredErr)). 670 WillReturnRows( 671 sqlmock.NewRows([]string{"id", "source", "src_schema_name", "src_table_name", "dst_schema_name", "dst_table_name", "data", "dst_data", "error_type", "status", "update_time"}).AddRow( 672 2, "mysql-replica", "srcdb", "srctbl", "dstdb", "dsttbl", "source data1", "unexpected data1", 2, 2, "2022-03-01", 673 ).AddRow( 674 3, "mysql-replica", "srcdb", "srctbl", "dstdb", "dsttbl", "source data2", "unexpected data2", 2, 2, "2022-03-01", 675 ), 676 ) 677 expected := [][]*pb.ValidationError{ 678 { 679 { 680 Id: "1", 681 Source: "mysql-replica", 682 SrcTable: "`srcdb`.`srctbl`", 683 DstTable: "`dstdb`.`dsttbl`", 684 SrcData: "source data", 685 DstData: "unexpected data", 686 ErrorType: "Column data not matched", 687 Status: pb.ValidateErrorState_NewErr, 688 Time: "2022-03-01", 689 }, 690 }, 691 { 692 { 693 Id: "2", 694 Source: "mysql-replica", 695 SrcTable: "`srcdb`.`srctbl`", 696 DstTable: "`dstdb`.`dsttbl`", 697 SrcData: "source data1", 698 DstData: "unexpected data1", 699 ErrorType: "Column data not matched", 700 Status: pb.ValidateErrorState_IgnoredErr, 701 Time: "2022-03-01", 702 }, 703 { 704 Id: "3", 705 Source: "mysql-replica", 706 SrcTable: "`srcdb`.`srctbl`", 707 DstTable: "`dstdb`.`dsttbl`", 708 SrcData: "source data2", 709 DstData: "unexpected data2", 710 ErrorType: "Column data not matched", 711 Status: pb.ValidateErrorState_IgnoredErr, 712 Time: "2022-03-01", 713 }, 714 }, 715 } 716 validator.persistHelper.db = conn.NewBaseDBForTest(db, func() {}) 717 res, err := validator.GetValidatorError(pb.ValidateErrorState_InvalidErr) 718 require.Nil(t, err) 719 require.EqualValues(t, expected[0], res) 720 res, err = validator.GetValidatorError(pb.ValidateErrorState_IgnoredErr) 721 require.Nil(t, err) 722 require.EqualValues(t, expected[1], res) 723 } 724 725 func TestValidatorOperateValidationError(t *testing.T) { 726 require.Nil(t, failpoint.Enable("github.com/pingcap/tiflow/dm/syncer/MockValidationQuery", `return(true)`)) 727 defer func() { 728 require.Nil(t, failpoint.Disable("github.com/pingcap/tiflow/dm/syncer/MockValidationQuery")) 729 }() 730 var err error 731 db, dbMock, err := sqlmock.New() 732 require.Equal(t, log.InitLogger(&log.Config{}), nil) 733 require.NoError(t, err) 734 cfg := genSubtaskConfig(t) 735 syncerObj := NewSyncer(cfg, nil, nil) 736 validator := NewContinuousDataValidator(cfg, syncerObj, false) 737 validator.ctx, validator.cancel = context.WithCancel(context.Background()) 738 validator.tctx = tcontext.NewContext(validator.ctx, validator.L) 739 validator.persistHelper.db = conn.NewBaseDBForTest(db, func() {}) 740 sourceID := validator.cfg.SourceID 741 // 1. clear all error 742 dbMock.ExpectExec("DELETE FROM " + validator.persistHelper.errorChangeTableName + " WHERE source=\\?"). 743 WithArgs(sourceID).WillReturnResult(sqlmock.NewResult(0, 1)) 744 // 2. clear error of errID 745 dbMock.ExpectExec("DELETE FROM "+validator.persistHelper.errorChangeTableName+" WHERE source=\\? AND id=\\?"). 746 WithArgs(sourceID, 1).WillReturnResult(sqlmock.NewResult(0, 1)) 747 // 3. mark all error as resolved 748 dbMock.ExpectExec("UPDATE "+validator.persistHelper.errorChangeTableName+" SET status=\\? WHERE source=\\?"). 749 WithArgs(int(pb.ValidateErrorState_ResolvedErr), sourceID).WillReturnResult(sqlmock.NewResult(0, 1)) 750 // 4. mark all error as ignored 751 dbMock.ExpectExec("UPDATE "+validator.persistHelper.errorChangeTableName+" SET status=\\? WHERE source=\\?"). 752 WithArgs(int(pb.ValidateErrorState_IgnoredErr), sourceID).WillReturnResult(sqlmock.NewResult(0, 1)) 753 // 5. mark error as resolved of errID 754 dbMock.ExpectExec("UPDATE "+validator.persistHelper.errorChangeTableName+" SET status=\\? WHERE source=\\? AND id=\\?"). 755 WithArgs(int(pb.ValidateErrorState_ResolvedErr), sourceID, 1).WillReturnResult(sqlmock.NewResult(0, 1)) 756 // 6. mark error as ignored of errID 757 dbMock.ExpectExec("UPDATE "+validator.persistHelper.errorChangeTableName+" SET status=\\? WHERE source=\\? AND id=\\?"). 758 WithArgs(int(pb.ValidateErrorState_IgnoredErr), sourceID, 1).WillReturnResult(sqlmock.NewResult(0, 1)) 759 760 // clear all error 761 err = validator.OperateValidatorError(pb.ValidationErrOp_ClearErrOp, 0, true) 762 require.NoError(t, err) 763 // clear error with id 764 err = validator.OperateValidatorError(pb.ValidationErrOp_ClearErrOp, 1, false) 765 require.NoError(t, err) 766 // mark all as resolved 767 err = validator.OperateValidatorError(pb.ValidationErrOp_ResolveErrOp, 0, true) 768 require.NoError(t, err) 769 // mark all as ignored 770 err = validator.OperateValidatorError(pb.ValidationErrOp_IgnoreErrOp, 0, true) 771 require.NoError(t, err) 772 // mark error as resolved with id 773 err = validator.OperateValidatorError(pb.ValidationErrOp_ResolveErrOp, 1, false) 774 require.NoError(t, err) 775 // mark error as ignored with id 776 err = validator.OperateValidatorError(pb.ValidationErrOp_IgnoreErrOp, 1, false) 777 require.NoError(t, err) 778 } 779 780 func TestValidatorMarkReachedSyncerRoutine(t *testing.T) { 781 cfg := genSubtaskConfig(t) 782 syncerObj := NewSyncer(cfg, nil, nil) 783 validator := NewContinuousDataValidator(cfg, syncerObj, false) 784 785 markErrorRowDelay = time.Minute 786 validator.ctx, validator.cancel = context.WithCancel(context.Background()) 787 require.False(t, validator.markErrorStarted.Load()) 788 validator.wg.Add(1) 789 go validator.markErrorStartedRoutine() 790 validator.cancel() 791 validator.wg.Wait() 792 require.False(t, validator.markErrorStarted.Load()) 793 794 markErrorRowDelay = time.Second 795 validator.ctx = context.Background() 796 require.False(t, validator.markErrorStarted.Load()) 797 validator.wg.Add(1) 798 go validator.markErrorStartedRoutine() 799 validator.wg.Wait() 800 require.True(t, validator.markErrorStarted.Load()) 801 } 802 803 func TestValidatorErrorProcessRoutineDeadlock(t *testing.T) { 804 cfg := genSubtaskConfig(t) 805 syncerObj := NewSyncer(cfg, nil, nil) 806 validator := NewContinuousDataValidator(cfg, syncerObj, false) 807 validator.ctx, validator.cancel = context.WithCancel(context.Background()) 808 validator.errChan = make(chan error) 809 validator.setStage(pb.Stage_Running) 810 validator.streamerController = binlogstream.NewStreamerController4Test(nil, nil) 811 validator.fromDB = &conn.BaseDB{} 812 validator.toDB = &conn.BaseDB{} 813 require.Equal(t, pb.Stage_Running, validator.Stage()) 814 815 finishedCh := make(chan struct{}) 816 // simulate a worker 817 validator.wg.Add(1) 818 go func() { 819 defer validator.wg.Done() 820 validator.sendError(errors.New("test error 1")) 821 validator.sendError(errors.New("test error 2")) 822 validator.sendError(errors.New("test error 3")) 823 }() 824 825 validator.errProcessWg.Add(1) 826 go func() { 827 defer func() { 828 finishedCh <- struct{}{} 829 }() 830 validator.errorProcessRoutine() 831 }() 832 833 select { 834 case <-finishedCh: 835 validator.errProcessWg.Wait() 836 require.Equal(t, pb.Stage_Stopped, validator.Stage()) 837 // all error gathered 838 require.Len(t, validator.getResult().Errors, 3) 839 case <-time.After(time.Second * 5): 840 t.Fatal("deadlock") 841 } 842 }