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  }