github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/shardddl/optimist_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 shardddl
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	tiddl "github.com/pingcap/tidb/pkg/ddl"
    23  	"github.com/pingcap/tidb/pkg/parser"
    24  	"github.com/pingcap/tidb/pkg/parser/ast"
    25  	"github.com/pingcap/tidb/pkg/parser/model"
    26  	"github.com/pingcap/tidb/pkg/sessionctx"
    27  	"github.com/pingcap/tidb/pkg/util/dbutil"
    28  	"github.com/pingcap/tidb/pkg/util/mock"
    29  	"github.com/pingcap/tiflow/dm/config"
    30  	"github.com/pingcap/tiflow/dm/config/dbconfig"
    31  	"github.com/pingcap/tiflow/dm/pb"
    32  	"github.com/pingcap/tiflow/dm/pkg/log"
    33  	"github.com/pingcap/tiflow/dm/pkg/shardddl/optimism"
    34  	"github.com/stretchr/testify/require"
    35  	"github.com/stretchr/testify/suite"
    36  	clientv3 "go.etcd.io/etcd/client/v3"
    37  	"go.etcd.io/etcd/tests/v3/integration"
    38  )
    39  
    40  func TestOptimistSuite(t *testing.T) {
    41  	suite.Run(t, new(testOptimistSuite))
    42  }
    43  
    44  type testOptimistSuite struct {
    45  	suite.Suite
    46  	mockCluster *integration.ClusterV3
    47  	etcdTestCli *clientv3.Client
    48  }
    49  
    50  func (t *testOptimistSuite) SetupSuite() {
    51  	require.NoError(t.T(), log.InitLogger(&log.Config{}))
    52  
    53  	integration.BeforeTestExternal(t.T())
    54  	t.mockCluster = integration.NewClusterV3(t.T(), &integration.ClusterConfig{Size: 1})
    55  	t.etcdTestCli = t.mockCluster.RandClient()
    56  }
    57  
    58  func (t *testOptimistSuite) TearDownSuite() {
    59  	t.mockCluster.Terminate(t.T())
    60  }
    61  
    62  func (t *testOptimistSuite) TearDownTest() {
    63  	t.clearOptimistTestSourceInfoOperation()
    64  }
    65  
    66  // clear keys in etcd test cluster.
    67  func (t *testOptimistSuite) clearOptimistTestSourceInfoOperation() {
    68  	require.NoError(t.T(), optimism.ClearTestInfoOperationColumn(t.etcdTestCli))
    69  }
    70  
    71  func createTableInfo(t *testing.T, p *parser.Parser, se sessionctx.Context, tableID int64, sql string) *model.TableInfo {
    72  	t.Helper()
    73  	node, err := p.ParseOneStmt(sql, "utf8mb4", "utf8mb4_bin")
    74  	if err != nil {
    75  		t.Fatalf("fail to parse stmt, %v", err)
    76  	}
    77  	createStmtNode, ok := node.(*ast.CreateTableStmt)
    78  	if !ok {
    79  		t.Fatalf("%s is not a CREATE TABLE statement", sql)
    80  	}
    81  	info, err := tiddl.MockTableInfo(se, createStmtNode, tableID)
    82  	if err != nil {
    83  		t.Fatalf("fail to create table info, %v", err)
    84  	}
    85  	return info
    86  }
    87  
    88  func watchExactOneOperation(
    89  	ctx context.Context,
    90  	cli *clientv3.Client,
    91  	task, source, upSchema, upTable string,
    92  	revision int64,
    93  ) (optimism.Operation, error) {
    94  	opCh := make(chan optimism.Operation, 10)
    95  	errCh := make(chan error, 10)
    96  	done := make(chan struct{})
    97  	subCtx, cancel := context.WithCancel(ctx)
    98  	go func() {
    99  		optimism.WatchOperationPut(subCtx, cli, task, source, upSchema, upTable, revision, opCh, errCh)
   100  		close(done)
   101  	}()
   102  	defer func() {
   103  		cancel()
   104  		<-done
   105  	}()
   106  
   107  	var op optimism.Operation
   108  	select {
   109  	case op = <-opCh:
   110  	case err := <-errCh:
   111  		return op, err
   112  	case <-ctx.Done():
   113  		return op, ctx.Err()
   114  	}
   115  
   116  	// Wait 100ms to check if there is unexpected operation.
   117  	select {
   118  	case extraOp := <-opCh:
   119  		return op, fmt.Errorf("unpexecped operation %s", extraOp)
   120  	case <-time.After(time.Millisecond * 100):
   121  	}
   122  	return op, nil
   123  }
   124  
   125  func checkLocks(t *testing.T, o *Optimist, expectedLocks []*pb.DDLLock, task string, sources []string) {
   126  	t.Helper()
   127  	lock, err := o.ShowLocks(task, sources)
   128  	require.NoError(t, err)
   129  	if expectedLocks == nil {
   130  		require.Len(t, lock, 0)
   131  	} else {
   132  		require.Equal(t, expectedLocks, lock)
   133  	}
   134  }
   135  
   136  func checkLocksByMap(t *testing.T, o *Optimist, expectedLocks map[string]*pb.DDLLock, sources []string, lockIDs ...string) {
   137  	t.Helper()
   138  	lock, err := o.ShowLocks("", sources)
   139  	require.NoError(t, err)
   140  	require.Len(t, lock, len(lockIDs))
   141  	lockIDMap := make(map[string]struct{})
   142  	for _, lockID := range lockIDs {
   143  		lockIDMap[lockID] = struct{}{}
   144  	}
   145  	for i := range lockIDs {
   146  		_, ok := lockIDMap[lock[i].ID]
   147  		require.True(t, ok)
   148  		delete(lockIDMap, lock[i].ID)
   149  		require.Equal(t, expectedLocks[lock[i].ID], lock[i])
   150  	}
   151  }
   152  
   153  func (t *testOptimistSuite) TestOptimistSourceTables() {
   154  	var (
   155  		logger     = log.L()
   156  		o          = NewOptimist(&logger, getDownstreamMeta)
   157  		task       = "task"
   158  		source1    = "mysql-replica-1"
   159  		source2    = "mysql-replica-2"
   160  		downSchema = "db"
   161  		downTable  = "tbl"
   162  		st1        = optimism.NewSourceTables(task, source1)
   163  		st2        = optimism.NewSourceTables(task, source2)
   164  	)
   165  
   166  	st1.AddTable("db", "tbl-1", downSchema, downTable)
   167  	st1.AddTable("db", "tbl-2", downSchema, downTable)
   168  	st2.AddTable("db", "tbl-1", downSchema, downTable)
   169  	st2.AddTable("db", "tbl-2", downSchema, downTable)
   170  
   171  	ctx, cancel := context.WithCancel(context.Background())
   172  	defer cancel()
   173  
   174  	// CASE 1: start without any previous kv and no etcd operation.
   175  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   176  	require.Nil(t.T(), o.tk.FindTables(task, downSchema, downTable))
   177  	o.Close()
   178  	o.Close() // close multiple times.
   179  
   180  	// CASE 2: start again without any previous kv.
   181  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   182  	require.Nil(t.T(), o.tk.FindTables(task, downSchema, downTable))
   183  
   184  	// PUT st1, should find tables.
   185  	_, err := optimism.PutSourceTables(t.etcdTestCli, st1)
   186  	require.NoError(t.T(), err)
   187  	require.Eventually(t.T(), func() bool {
   188  		tts := o.tk.FindTables(task, downSchema, downTable)
   189  		return len(tts) == 1
   190  	}, 30*100*time.Millisecond, 100*time.Millisecond)
   191  	tts := o.tk.FindTables(task, downSchema, downTable)
   192  	require.Len(t.T(), tts, 1)
   193  	require.Equal(t.T(), st1.TargetTable(downSchema, downTable), tts[0])
   194  	o.Close()
   195  
   196  	// CASE 3: start again with previous source tables.
   197  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   198  	tts = o.tk.FindTables(task, downSchema, downTable)
   199  	require.Len(t.T(), tts, 1)
   200  	require.Equal(t.T(), st1.TargetTable(downSchema, downTable), tts[0])
   201  
   202  	// PUT st2, should find more tables.
   203  	_, err = optimism.PutSourceTables(t.etcdTestCli, st2)
   204  	require.NoError(t.T(), err)
   205  	require.Eventually(t.T(), func() bool {
   206  		tts = o.tk.FindTables(task, downSchema, downTable)
   207  		return len(tts) == 2
   208  	}, 30*100*time.Millisecond, 100*time.Millisecond)
   209  	tts = o.tk.FindTables(task, downSchema, downTable)
   210  	require.Len(t.T(), tts, 2)
   211  	require.Equal(t.T(), st1.TargetTable(downSchema, downTable), tts[0])
   212  	require.Equal(t.T(), st2.TargetTable(downSchema, downTable), tts[1])
   213  	o.Close()
   214  
   215  	// CASE 4: create (not re-start) a new optimist with previous source tables.
   216  	o = NewOptimist(&logger, getDownstreamMeta)
   217  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   218  	tts = o.tk.FindTables(task, downSchema, downTable)
   219  	require.Len(t.T(), tts, 2)
   220  	require.Equal(t.T(), st1.TargetTable(downSchema, downTable), tts[0])
   221  	require.Equal(t.T(), st2.TargetTable(downSchema, downTable), tts[1])
   222  
   223  	// DELETE st1, should find less tables.
   224  	_, err = optimism.DeleteSourceTables(t.etcdTestCli, st1)
   225  	require.NoError(t.T(), err)
   226  	require.Eventually(t.T(), func() bool {
   227  		tts = o.tk.FindTables(task, downSchema, downTable)
   228  		return len(tts) == 1
   229  	}, 30*100*time.Millisecond, 100*time.Millisecond)
   230  	tts = o.tk.FindTables(task, downSchema, downTable)
   231  	require.Len(t.T(), tts, 1)
   232  	require.Equal(t.T(), st2.TargetTable(downSchema, downTable), tts[0])
   233  	o.Close()
   234  }
   235  
   236  func (t *testOptimistSuite) TestOptimist() {
   237  	t.testOptimist(t.etcdTestCli, noRestart)
   238  	t.testOptimist(t.etcdTestCli, restartOnly)
   239  	t.testOptimist(t.etcdTestCli, restartNewInstance)
   240  	t.testSortInfos(t.etcdTestCli)
   241  }
   242  
   243  func (t *testOptimistSuite) testOptimist(cli *clientv3.Client, restart int) {
   244  	defer func() {
   245  		require.NoError(t.T(), optimism.ClearTestInfoOperationColumn(cli))
   246  	}()
   247  
   248  	var (
   249  		tick    = 100 * time.Millisecond
   250  		waitFor = 30 * tick
   251  		logger  = log.L()
   252  		o       = NewOptimist(&logger, getDownstreamMeta)
   253  
   254  		rebuildOptimist = func(ctx context.Context) {
   255  			switch restart {
   256  			case restartOnly:
   257  				o.Close()
   258  				require.NoError(t.T(), o.Start(ctx, cli))
   259  			case restartNewInstance:
   260  				o.Close()
   261  				o = NewOptimist(&logger, getDownstreamMeta)
   262  				require.NoError(t.T(), o.Start(ctx, cli))
   263  			}
   264  		}
   265  
   266  		task             = "task-test-optimist"
   267  		source1          = "mysql-replica-1"
   268  		source2          = "mysql-replica-2"
   269  		downSchema       = "foo"
   270  		downTable        = "bar"
   271  		lockID           = fmt.Sprintf("%s-`%s`.`%s`", task, downSchema, downTable)
   272  		st1              = optimism.NewSourceTables(task, source1)
   273  		st31             = optimism.NewSourceTables(task, source1)
   274  		st32             = optimism.NewSourceTables(task, source2)
   275  		p                = parser.New()
   276  		se               = mock.NewContext()
   277  		tblID      int64 = 111
   278  		DDLs1            = []string{"ALTER TABLE bar ADD COLUMN c1 INT"}
   279  		DDLs2            = []string{"ALTER TABLE bar ADD COLUMN c2 INT"}
   280  		DDLs3            = []string{"ALTER TABLE bar DROP COLUMN c2"}
   281  		ti0              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
   282  		ti1              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 INT)`)
   283  		ti2              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 INT, c2 INT)`)
   284  		ti3              = ti1
   285  		i11              = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
   286  		i12              = optimism.NewInfo(task, source1, "foo", "bar-2", downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
   287  		i21              = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, DDLs2, ti1, []*model.TableInfo{ti2})
   288  		i23              = optimism.NewInfo(task, source2, "foo-2", "bar-3", downSchema, downTable, DDLs2, ti1, []*model.TableInfo{ti2})
   289  		i31              = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, DDLs3, ti2, []*model.TableInfo{ti3})
   290  		i33              = optimism.NewInfo(task, source2, "foo-2", "bar-3", downSchema, downTable, DDLs3, ti2, []*model.TableInfo{ti3})
   291  	)
   292  
   293  	st1.AddTable("foo", "bar-1", downSchema, downTable)
   294  	st1.AddTable("foo", "bar-2", downSchema, downTable)
   295  	st31.AddTable("foo", "bar-1", downSchema, downTable)
   296  	st32.AddTable("foo-2", "bar-3", downSchema, downTable)
   297  
   298  	// put source tables first.
   299  	_, err := optimism.PutSourceTables(cli, st1)
   300  	require.NoError(t.T(), err)
   301  
   302  	ctx, cancel := context.WithCancel(context.Background())
   303  	defer cancel()
   304  
   305  	// CASE 1: start without any previous shard DDL info.
   306  	require.NoError(t.T(), o.Start(ctx, cli))
   307  	require.Len(t.T(), o.Locks(), 0)
   308  	o.Close()
   309  	o.Close() // close multiple times.
   310  
   311  	// CASE 2: start again without any previous shard DDL info.
   312  	require.NoError(t.T(), o.Start(ctx, cli))
   313  	require.Len(t.T(), o.Locks(), 0)
   314  
   315  	// PUT i11, will create a lock but not synced.
   316  	rev1, err := optimism.PutInfo(cli, i11)
   317  	require.NoError(t.T(), err)
   318  	require.Eventually(t.T(), func() bool {
   319  		return len(o.Locks()) == 1
   320  	}, waitFor, tick)
   321  	require.Contains(t.T(), o.Locks(), lockID)
   322  	synced, remain := o.Locks()[lockID].IsSynced()
   323  	require.False(t.T(), synced)
   324  	require.Equal(t.T(), 1, remain)
   325  
   326  	// check ShowLocks.
   327  	expectedLock := []*pb.DDLLock{
   328  		{
   329  			ID:    lockID,
   330  			Task:  task,
   331  			Mode:  config.ShardOptimistic,
   332  			Owner: "",
   333  			DDLs:  nil,
   334  			Synced: []string{
   335  				fmt.Sprintf("%s-%s", i11.Source, dbutil.TableName(i11.UpSchema, i11.UpTable)),
   336  			},
   337  			Unsynced: []string{
   338  				fmt.Sprintf("%s-%s", i12.Source, dbutil.TableName(i12.UpSchema, i12.UpTable)),
   339  			},
   340  		},
   341  	}
   342  	checkLocks(t.T(), o, expectedLock, "", []string{})
   343  
   344  	// wait operation for i11 become available.
   345  	op11, err := watchExactOneOperation(ctx, cli, i11.Task, i11.Source, i11.UpSchema, i11.UpTable, rev1)
   346  	require.NoError(t.T(), err)
   347  	require.Equal(t.T(), DDLs1, op11.DDLs)
   348  	require.Equal(t.T(), optimism.ConflictNone, op11.ConflictStage)
   349  	checkLocks(t.T(), o, expectedLock, "", []string{})
   350  
   351  	// mark op11 as done.
   352  	op11c := op11
   353  	op11c.Done = true
   354  	_, putted, err := optimism.PutOperation(cli, false, op11c, 0)
   355  	require.NoError(t.T(), err)
   356  	require.True(t.T(), putted)
   357  	require.Eventually(t.T(), func() bool {
   358  		lock := o.Locks()[lockID]
   359  		if lock == nil {
   360  			return false
   361  		}
   362  		return lock.IsDone(op11.Source, op11.UpSchema, op11.UpTable)
   363  	}, waitFor, tick)
   364  	require.False(t.T(), o.Locks()[lockID].IsDone(i12.Source, i12.UpSchema, i12.UpTable))
   365  	checkLocks(t.T(), o, expectedLock, "", []string{})
   366  
   367  	// PUT i12, the lock will be synced.
   368  	rev2, err := optimism.PutInfo(cli, i12)
   369  	require.NoError(t.T(), err)
   370  	require.Eventually(t.T(), func() bool {
   371  		synced, _ = o.Locks()[lockID].IsSynced()
   372  		return synced
   373  	}, waitFor, tick)
   374  
   375  	expectedLock = []*pb.DDLLock{
   376  		{
   377  			ID:    lockID,
   378  			Task:  task,
   379  			Mode:  config.ShardOptimistic,
   380  			Owner: "",
   381  			DDLs:  nil,
   382  			Synced: []string{
   383  				fmt.Sprintf("%s-%s", i11.Source, dbutil.TableName(i11.UpSchema, i11.UpTable)),
   384  				fmt.Sprintf("%s-%s", i12.Source, dbutil.TableName(i12.UpSchema, i12.UpTable)),
   385  			},
   386  			Unsynced: []string{},
   387  		},
   388  	}
   389  	checkLocks(t.T(), o, expectedLock, "", []string{})
   390  
   391  	// wait operation for i12 become available.
   392  	op12, err := watchExactOneOperation(ctx, cli, i12.Task, i12.Source, i12.UpSchema, i12.UpTable, rev2)
   393  	require.NoError(t.T(), err)
   394  	require.Equal(t.T(), DDLs1, op12.DDLs)
   395  	require.Equal(t.T(), optimism.ConflictNone, op12.ConflictStage)
   396  	checkLocks(t.T(), o, expectedLock, "", []string{})
   397  
   398  	// mark op12 as done, the lock should be resolved.
   399  	op12c := op12
   400  	op12c.Done = true
   401  	_, putted, err = optimism.PutOperation(cli, false, op12c, 0)
   402  	require.NoError(t.T(), err)
   403  	require.True(t.T(), putted)
   404  	require.Eventually(t.T(), func() bool {
   405  		_, ok := o.Locks()[lockID]
   406  		return !ok
   407  	}, waitFor, tick)
   408  	require.Len(t.T(), o.Locks(), 0)
   409  	checkLocks(t.T(), o, nil, "", nil)
   410  
   411  	// no shard DDL info or lock operation exists.
   412  	ifm, _, err := optimism.GetAllInfo(cli)
   413  	require.NoError(t.T(), err)
   414  	require.Len(t.T(), ifm, 0)
   415  	opm, _, err := optimism.GetAllOperations(cli)
   416  	require.NoError(t.T(), err)
   417  	require.Len(t.T(), opm, 0)
   418  
   419  	// put another table info.
   420  	rev1, err = optimism.PutInfo(cli, i21)
   421  	require.NoError(t.T(), err)
   422  	require.Eventually(t.T(), func() bool {
   423  		return len(o.Locks()) == 1
   424  	}, waitFor, tick)
   425  	require.Contains(t.T(), o.Locks(), lockID)
   426  	synced, remain = o.Locks()[lockID].IsSynced()
   427  	require.False(t.T(), synced)
   428  	require.Equal(t.T(), 1, remain)
   429  
   430  	// wait operation for i21 become available.
   431  	op21, err := watchExactOneOperation(ctx, cli, i21.Task, i21.Source, i21.UpSchema, i21.UpTable, rev1)
   432  	require.NoError(t.T(), err)
   433  	require.Equal(t.T(), i21.DDLs, op21.DDLs)
   434  	require.Equal(t.T(), optimism.ConflictNone, op12.ConflictStage)
   435  
   436  	// CASE 3: start again with some previous shard DDL info and the lock is un-synced.
   437  	rebuildOptimist(ctx)
   438  	require.Len(t.T(), o.Locks(), 1)
   439  	require.Contains(t.T(), o.Locks(), lockID)
   440  	synced, remain = o.Locks()[lockID].IsSynced()
   441  	require.False(t.T(), synced)
   442  	require.Equal(t.T(), 1, remain)
   443  
   444  	// put table info for a new table (to simulate `CREATE TABLE`).
   445  	// lock will fetch the new table info from downstream.
   446  	// here will use ti1
   447  	_, err = optimism.PutSourceTables(cli, st32)
   448  	require.NoError(t.T(), err)
   449  	require.Eventually(t.T(), func() bool {
   450  		ready := o.Locks()[lockID].Ready()
   451  		return !ready[source2][i23.UpSchema][i23.UpTable]
   452  	}, waitFor, tick)
   453  	synced, remain = o.Locks()[lockID].IsSynced()
   454  	require.False(t.T(), synced)
   455  	require.Equal(t.T(), remain, 2)
   456  	tts := o.tk.FindTables(task, downSchema, downTable)
   457  	require.Len(t.T(), tts, 2)
   458  	require.Equal(t.T(), source2, tts[1].Source)
   459  	require.Contains(t.T(), tts[1].UpTables, i23.UpSchema)
   460  	require.Contains(t.T(), tts[1].UpTables[i23.UpSchema], i23.UpTable)
   461  
   462  	// ddl for new table
   463  	rev3, err := optimism.PutInfo(cli, i23)
   464  	require.NoError(t.T(), err)
   465  	// wait operation for i23 become available.
   466  	op23, err := watchExactOneOperation(ctx, cli, i23.Task, i23.Source, i23.UpSchema, i23.UpTable, rev3)
   467  	require.NoError(t.T(), err)
   468  	require.Equal(t.T(), op23.DDLs, i23.DDLs)
   469  	require.Equal(t.T(), op23.ConflictStage, optimism.ConflictNone)
   470  
   471  	require.Contains(t.T(), o.Locks(), lockID)
   472  	synced, remain = o.Locks()[lockID].IsSynced()
   473  	require.False(t.T(), synced)
   474  	require.Equal(t.T(), remain, 1)
   475  
   476  	// check ShowLocks.
   477  	expectedLock = []*pb.DDLLock{
   478  		{
   479  			ID:    lockID,
   480  			Task:  task,
   481  			Mode:  config.ShardOptimistic,
   482  			Owner: "",
   483  			DDLs:  nil,
   484  			Synced: []string{
   485  				fmt.Sprintf("%s-%s", source1, dbutil.TableName(i21.UpSchema, i21.UpTable)),
   486  				fmt.Sprintf("%s-%s", source2, dbutil.TableName(i23.UpSchema, i23.UpTable)),
   487  			},
   488  			Unsynced: []string{
   489  				fmt.Sprintf("%s-%s", source1, dbutil.TableName(i12.UpSchema, i12.UpTable)),
   490  			},
   491  		},
   492  	}
   493  	checkLocks(t.T(), o, expectedLock, "", []string{})
   494  	checkLocks(t.T(), o, expectedLock, task, []string{})
   495  	checkLocks(t.T(), o, expectedLock, "", []string{source1})
   496  	checkLocks(t.T(), o, expectedLock, "", []string{source2})
   497  	checkLocks(t.T(), o, expectedLock, "", []string{source1, source2})
   498  	checkLocks(t.T(), o, expectedLock, task, []string{source1, source2})
   499  	checkLocks(t.T(), o, nil, "not-exist", []string{})
   500  	checkLocks(t.T(), o, nil, "", []string{"not-exist"})
   501  
   502  	// delete i12 for a table (to simulate `DROP TABLE`), the lock should become synced again.
   503  	rev2, err = optimism.PutInfo(cli, i12) // put i12 first to trigger DELETE for i12.
   504  	require.NoError(t.T(), err)
   505  	// wait until operation for i12 ready.
   506  	_, err = watchExactOneOperation(ctx, cli, i12.Task, i12.Source, i12.UpSchema, i12.UpTable, rev2)
   507  	require.NoError(t.T(), err)
   508  
   509  	_, err = optimism.PutSourceTables(cli, st31)
   510  	require.NoError(t.T(), err)
   511  	require.Eventually(t.T(), func() bool {
   512  		synced, _ = o.Locks()[lockID].IsSynced()
   513  		return synced
   514  	}, waitFor, tick)
   515  	tts = o.tk.FindTables(task, downSchema, downTable)
   516  	require.Len(t.T(), tts, 2)
   517  	require.Equal(t.T(), source1, tts[0].Source)
   518  	require.Len(t.T(), tts[0].UpTables, 1)
   519  	require.Contains(t.T(), tts[0].UpTables[i21.UpSchema], i21.UpTable)
   520  	require.Equal(t.T(), source2, tts[1].Source)
   521  	require.Len(t.T(), tts[1].UpTables, 1)
   522  	require.Contains(t.T(), tts[1].UpTables[i23.UpSchema], i23.UpTable)
   523  	require.False(t.T(), o.Locks()[lockID].IsResolved())
   524  	require.False(t.T(), o.Locks()[lockID].IsDone(i21.Source, i21.UpSchema, i21.UpTable))
   525  	require.False(t.T(), o.Locks()[lockID].IsDone(i23.Source, i23.UpSchema, i23.UpTable))
   526  
   527  	// CASE 4: start again with some previous shard DDL info and non-`done` operation.
   528  	rebuildOptimist(ctx)
   529  	require.Len(t.T(), o.Locks(), 1)
   530  	require.Contains(t.T(), o.Locks(), lockID)
   531  	synced, remain = o.Locks()[lockID].IsSynced()
   532  	require.True(t.T(), synced)
   533  	require.Equal(t.T(), 0, remain)
   534  	require.False(t.T(), o.Locks()[lockID].IsDone(i21.Source, i21.UpSchema, i21.UpTable))
   535  	require.False(t.T(), o.Locks()[lockID].IsDone(i23.Source, i23.UpSchema, i23.UpTable))
   536  
   537  	// mark op21 as done.
   538  	op21c := op21
   539  	op21c.Done = true
   540  	_, putted, err = optimism.PutOperation(cli, false, op21c, 0)
   541  	require.NoError(t.T(), err)
   542  	require.True(t.T(), putted)
   543  	require.Eventually(t.T(), func() bool {
   544  		return o.Locks()[lockID].IsDone(i21.Source, i21.UpSchema, i21.UpTable)
   545  	}, waitFor, tick)
   546  
   547  	// CASE 5: start again with some previous shard DDL info and `done` operation.
   548  	rebuildOptimist(ctx)
   549  	require.Len(t.T(), o.Locks(), 1)
   550  	require.Contains(t.T(), o.Locks(), lockID)
   551  	synced, remain = o.Locks()[lockID].IsSynced()
   552  	require.True(t.T(), synced)
   553  	require.Equal(t.T(), 0, remain)
   554  	require.True(t.T(), o.Locks()[lockID].IsDone(i21.Source, i21.UpSchema, i21.UpTable))
   555  	require.False(t.T(), o.Locks()[lockID].IsDone(i23.Source, i23.UpSchema, i23.UpTable))
   556  
   557  	// mark op23 as done.
   558  	op23c := op23
   559  	op23c.Done = true
   560  	_, putted, err = optimism.PutOperation(cli, false, op23c, 0)
   561  	require.NoError(t.T(), err)
   562  	require.True(t.T(), putted)
   563  	require.Eventually(t.T(), func() bool {
   564  		_, ok := o.Locks()[lockID]
   565  		return !ok
   566  	}, waitFor, tick)
   567  	require.Len(t.T(), o.Locks(), 0)
   568  
   569  	// PUT i31, will create a lock but not synced (to test `DROP COLUMN`)
   570  	rev1, err = optimism.PutInfo(cli, i31)
   571  	require.NoError(t.T(), err)
   572  	require.Eventually(t.T(), func() bool {
   573  		return len(o.Locks()) == 1
   574  	}, waitFor, tick)
   575  	require.Contains(t.T(), o.Locks(), lockID)
   576  	synced, remain = o.Locks()[lockID].IsSynced()
   577  	require.False(t.T(), synced)
   578  	require.Equal(t.T(), 1, remain)
   579  	// check ShowLocks.
   580  	expectedLock = []*pb.DDLLock{
   581  		{
   582  			ID:    lockID,
   583  			Task:  task,
   584  			Mode:  config.ShardOptimistic,
   585  			Owner: "",
   586  			DDLs:  nil,
   587  			Synced: []string{ // for `DROP COLUMN`, un-dropped is synced (the same with the joined schema)
   588  				fmt.Sprintf("%s-%s", i33.Source, dbutil.TableName(i33.UpSchema, i33.UpTable)),
   589  			},
   590  			Unsynced: []string{ // for `DROP COLUMN`, dropped is un-synced (not the same with the joined schema)
   591  				fmt.Sprintf("%s-%s", i31.Source, dbutil.TableName(i31.UpSchema, i31.UpTable)),
   592  			},
   593  		},
   594  	}
   595  	checkLocks(t.T(), o, expectedLock, "", []string{})
   596  
   597  	// wait operation for i31 become available.
   598  	op31, err := watchExactOneOperation(ctx, cli, i31.Task, i31.Source, i31.UpSchema, i31.UpTable, rev1)
   599  	require.NoError(t.T(), err)
   600  	require.Equal(t.T(), []string{}, op31.DDLs)
   601  	require.Equal(t.T(), optimism.ConflictNone, op31.ConflictStage)
   602  	checkLocks(t.T(), o, expectedLock, "", []string{})
   603  
   604  	// mark op31 as done.
   605  	op31c := op31
   606  	op31c.Done = true
   607  	_, putted, err = optimism.PutOperation(cli, false, op31c, 0)
   608  	require.NoError(t.T(), err)
   609  	require.True(t.T(), putted)
   610  	require.Eventually(t.T(), func() bool {
   611  		return o.Locks()[lockID].IsDone(op31c.Source, op31c.UpSchema, op31c.UpTable)
   612  	}, waitFor, tick)
   613  	checkLocks(t.T(), o, expectedLock, "", []string{})
   614  
   615  	// PUT i33, the lock will be synced.
   616  	rev3, err = optimism.PutInfo(cli, i33)
   617  	require.NoError(t.T(), err)
   618  	require.Eventually(t.T(), func() bool {
   619  		synced, _ = o.Locks()[lockID].IsSynced()
   620  		return synced
   621  	}, waitFor, tick)
   622  
   623  	expectedLock = []*pb.DDLLock{
   624  		{
   625  			ID:    lockID,
   626  			Task:  task,
   627  			Mode:  config.ShardOptimistic,
   628  			Owner: "",
   629  			DDLs:  nil,
   630  			Synced: []string{
   631  				fmt.Sprintf("%s-%s", i31.Source, dbutil.TableName(i31.UpSchema, i31.UpTable)),
   632  				fmt.Sprintf("%s-%s", i33.Source, dbutil.TableName(i33.UpSchema, i33.UpTable)),
   633  			},
   634  			Unsynced: []string{},
   635  		},
   636  	}
   637  	checkLocks(t.T(), o, expectedLock, "", []string{})
   638  
   639  	// wait operation for i33 become available.
   640  	op33, err := watchExactOneOperation(ctx, cli, i33.Task, i33.Source, i33.UpSchema, i33.UpTable, rev3)
   641  	require.NoError(t.T(), err)
   642  	require.Equal(t.T(), DDLs3, op33.DDLs)
   643  	require.Equal(t.T(), optimism.ConflictNone, op33.ConflictStage)
   644  	checkLocks(t.T(), o, expectedLock, "", []string{})
   645  
   646  	// mark op33 as done, the lock should be resolved.
   647  	op33c := op33
   648  	op33c.Done = true
   649  	_, putted, err = optimism.PutOperation(cli, false, op33c, 0)
   650  	require.NoError(t.T(), err)
   651  	require.True(t.T(), putted)
   652  	require.Eventually(t.T(), func() bool {
   653  		_, ok := o.Locks()[lockID]
   654  		return !ok
   655  	}, waitFor, tick)
   656  	require.Len(t.T(), o.Locks(), 0)
   657  	checkLocks(t.T(), o, nil, "", nil)
   658  
   659  	// CASE 6: start again after all shard DDL locks have been resolved.
   660  	rebuildOptimist(ctx)
   661  	require.Len(t.T(), o.Locks(), 0)
   662  	o.Close()
   663  }
   664  
   665  func (t *testOptimistSuite) TestOptimistLockConflict() {
   666  	var (
   667  		watchTimeout       = 5 * time.Second
   668  		logger             = log.L()
   669  		o                  = NewOptimist(&logger, getDownstreamMeta)
   670  		task               = "task-test-optimist"
   671  		source1            = "mysql-replica-1"
   672  		downSchema         = "foo"
   673  		downTable          = "bar"
   674  		st1                = optimism.NewSourceTables(task, source1)
   675  		p                  = parser.New()
   676  		se                 = mock.NewContext()
   677  		tblID        int64 = 222
   678  		DDLs1              = []string{"ALTER TABLE bar ADD COLUMN c1 TEXT"}
   679  		DDLs2              = []string{"ALTER TABLE bar ADD COLUMN c1 DATETIME"}
   680  		ti0                = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
   681  		ti1                = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT)`)
   682  		ti2                = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 DATETIME)`)
   683  		ti3                = ti0
   684  		i1                 = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
   685  		i2                 = optimism.NewInfo(task, source1, "foo", "bar-2", downSchema, downTable, DDLs2, ti0, []*model.TableInfo{ti2})
   686  		i3                 = optimism.NewInfo(task, source1, "foo", "bar-2", downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti3})
   687  	)
   688  
   689  	st1.AddTable("foo", "bar-1", downSchema, downTable)
   690  	st1.AddTable("foo", "bar-2", downSchema, downTable)
   691  
   692  	// put source tables first.
   693  	_, err := optimism.PutSourceTables(t.etcdTestCli, st1)
   694  	require.NoError(t.T(), err)
   695  
   696  	ctx, cancel := context.WithCancel(context.Background())
   697  	defer func() {
   698  		cancel()
   699  		o.Close()
   700  	}()
   701  
   702  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   703  	require.Len(t.T(), o.Locks(), 0)
   704  
   705  	// PUT i1, will create a lock but not synced.
   706  	rev1, err := optimism.PutInfo(t.etcdTestCli, i1)
   707  	require.NoError(t.T(), err)
   708  	// wait operation for i1 become available.
   709  	opCh := make(chan optimism.Operation, 10)
   710  	errCh := make(chan error, 10)
   711  	ctx2, cancel2 := context.WithCancel(ctx)
   712  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i1.Task, i1.Source, i1.UpSchema, i1.UpTable, rev1, opCh, errCh)
   713  	select {
   714  	case <-time.After(watchTimeout):
   715  		t.T().Fatal("timeout")
   716  	case op1 := <-opCh:
   717  		require.Equal(t.T(), DDLs1, op1.DDLs)
   718  		require.Equal(t.T(), optimism.ConflictNone, op1.ConflictStage)
   719  	}
   720  
   721  	cancel2()
   722  	close(opCh)
   723  	close(errCh)
   724  	require.Equal(t.T(), 0, len(opCh))
   725  	require.Equal(t.T(), 0, len(errCh))
   726  
   727  	// PUT i2, conflict will be detected.
   728  	rev2, err := optimism.PutInfo(t.etcdTestCli, i2)
   729  	require.NoError(t.T(), err)
   730  	// wait operation for i2 become available.
   731  	opCh = make(chan optimism.Operation, 10)
   732  	errCh = make(chan error, 10)
   733  
   734  	ctx2, cancel2 = context.WithCancel(ctx)
   735  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i2.Task, i2.Source, i2.UpSchema, i2.UpTable, rev2, opCh, errCh)
   736  	select {
   737  	case <-time.After(watchTimeout):
   738  		t.T().Fatal("timeout")
   739  	case op2 := <-opCh:
   740  		require.Equal(t.T(), []string{}, op2.DDLs)
   741  		require.Equal(t.T(), optimism.ConflictDetected, op2.ConflictStage)
   742  	}
   743  
   744  	cancel2()
   745  	close(opCh)
   746  	close(errCh)
   747  	require.Equal(t.T(), 0, len(opCh))
   748  	require.Equal(t.T(), 0, len(errCh))
   749  
   750  	// PUT i3, no conflict now.
   751  	// case for handle-error replace
   752  	rev3, err := optimism.PutInfo(t.etcdTestCli, i3)
   753  	require.NoError(t.T(), err)
   754  	// wait operation for i3 become available.
   755  	opCh = make(chan optimism.Operation, 10)
   756  	errCh = make(chan error, 10)
   757  	ctx2, cancel2 = context.WithCancel(ctx)
   758  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i3.Task, i3.Source, i3.UpSchema, i3.UpTable, rev3, opCh, errCh)
   759  	select {
   760  	case <-time.After(watchTimeout):
   761  		t.T().Fatal("timeout")
   762  	case op3 := <-opCh:
   763  		require.Equal(t.T(), DDLs1, op3.DDLs)
   764  		require.Equal(t.T(), optimism.ConflictNone, op3.ConflictStage)
   765  	}
   766  	cancel2()
   767  	close(opCh)
   768  	close(errCh)
   769  	require.Equal(t.T(), 0, len(opCh))
   770  	require.Equal(t.T(), 0, len(errCh))
   771  }
   772  
   773  func (t *testOptimistSuite) TestOptimistLockMultipleTarget() {
   774  	var (
   775  		tick               = 100 * time.Millisecond
   776  		waitFor            = 30 * tick
   777  		watchTimeout       = 5 * time.Second
   778  		logger             = log.L()
   779  		o                  = NewOptimist(&logger, getDownstreamMeta)
   780  		task               = "test-optimist-lock-multiple-target"
   781  		source             = "mysql-replica-1"
   782  		upSchema           = "foo"
   783  		upTables           = []string{"bar-1", "bar-2", "bar-3", "bar-4"}
   784  		downSchema         = "foo"
   785  		downTable1         = "bar"
   786  		downTable2         = "rab"
   787  		lockID1            = fmt.Sprintf("%s-`%s`.`%s`", task, downSchema, downTable1)
   788  		lockID2            = fmt.Sprintf("%s-`%s`.`%s`", task, downSchema, downTable2)
   789  		sts                = optimism.NewSourceTables(task, source)
   790  		p                  = parser.New()
   791  		se                 = mock.NewContext()
   792  		tblID        int64 = 111
   793  		DDLs               = []string{"ALTER TABLE bar ADD COLUMN c1 TEXT"}
   794  		ti0                = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
   795  		ti1                = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT)`)
   796  		i11                = optimism.NewInfo(task, source, upSchema, upTables[0], downSchema, downTable1, DDLs, ti0, []*model.TableInfo{ti1})
   797  		i12                = optimism.NewInfo(task, source, upSchema, upTables[1], downSchema, downTable1, DDLs, ti0, []*model.TableInfo{ti1})
   798  		i21                = optimism.NewInfo(task, source, upSchema, upTables[2], downSchema, downTable2, DDLs, ti0, []*model.TableInfo{ti1})
   799  		i22                = optimism.NewInfo(task, source, upSchema, upTables[3], downSchema, downTable2, DDLs, ti0, []*model.TableInfo{ti1})
   800  	)
   801  
   802  	sts.AddTable(upSchema, upTables[0], downSchema, downTable1)
   803  	sts.AddTable(upSchema, upTables[1], downSchema, downTable1)
   804  	sts.AddTable(upSchema, upTables[2], downSchema, downTable2)
   805  	sts.AddTable(upSchema, upTables[3], downSchema, downTable2)
   806  
   807  	// put source tables first.
   808  	_, err := optimism.PutSourceTables(t.etcdTestCli, sts)
   809  	require.NoError(t.T(), err)
   810  
   811  	ctx, cancel := context.WithCancel(context.Background())
   812  	defer func() {
   813  		cancel()
   814  		o.Close()
   815  	}()
   816  
   817  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
   818  	require.Len(t.T(), o.Locks(), 0)
   819  
   820  	// PUT i11 and i21, will create two locks but no synced.
   821  	_, err = optimism.PutInfo(t.etcdTestCli, i11)
   822  	require.NoError(t.T(), err)
   823  	_, err = optimism.PutInfo(t.etcdTestCli, i21)
   824  	require.NoError(t.T(), err)
   825  	require.Eventually(t.T(), func() bool {
   826  		return len(o.Locks()) == 2
   827  	}, waitFor, tick)
   828  	require.Contains(t.T(), o.Locks(), lockID1)
   829  	require.Contains(t.T(), o.Locks(), lockID2)
   830  
   831  	// check ShowLocks
   832  	expectedLock := map[string]*pb.DDLLock{
   833  		lockID1: {
   834  			ID:    lockID1,
   835  			Task:  task,
   836  			Mode:  config.ShardOptimistic,
   837  			Owner: "",
   838  			DDLs:  nil,
   839  			Synced: []string{
   840  				fmt.Sprintf("%s-%s", i11.Source, dbutil.TableName(i11.UpSchema, i11.UpTable)),
   841  			},
   842  			Unsynced: []string{
   843  				fmt.Sprintf("%s-%s", i12.Source, dbutil.TableName(i12.UpSchema, i12.UpTable)),
   844  			},
   845  		},
   846  		lockID2: {
   847  			ID:    lockID2,
   848  			Task:  task,
   849  			Mode:  config.ShardOptimistic,
   850  			Owner: "",
   851  			DDLs:  nil,
   852  			Synced: []string{
   853  				fmt.Sprintf("%s-%s", i21.Source, dbutil.TableName(i21.UpSchema, i21.UpTable)),
   854  			},
   855  			Unsynced: []string{
   856  				fmt.Sprintf("%s-%s", i22.Source, dbutil.TableName(i22.UpSchema, i22.UpTable)),
   857  			},
   858  		},
   859  	}
   860  	checkLocksByMap(t.T(), o, expectedLock, []string{}, lockID1, lockID2)
   861  
   862  	// put i12 and i22, both of locks will be synced.
   863  	rev1, err := optimism.PutInfo(t.etcdTestCli, i12)
   864  	require.NoError(t.T(), err)
   865  	rev2, err := optimism.PutInfo(t.etcdTestCli, i22)
   866  	require.NoError(t.T(), err)
   867  	require.Eventually(t.T(), func() bool {
   868  		synced1, _ := o.Locks()[lockID1].IsSynced()
   869  		synced2, _ := o.Locks()[lockID2].IsSynced()
   870  		return synced1 && synced2
   871  	}, waitFor, tick)
   872  
   873  	expectedLock[lockID1].Synced = []string{
   874  		fmt.Sprintf("%s-%s", i11.Source, dbutil.TableName(i11.UpSchema, i11.UpTable)),
   875  		fmt.Sprintf("%s-%s", i12.Source, dbutil.TableName(i12.UpSchema, i12.UpTable)),
   876  	}
   877  	expectedLock[lockID1].Unsynced = []string{}
   878  	expectedLock[lockID2].Synced = []string{
   879  		fmt.Sprintf("%s-%s", i21.Source, dbutil.TableName(i21.UpSchema, i21.UpTable)),
   880  		fmt.Sprintf("%s-%s", i22.Source, dbutil.TableName(i22.UpSchema, i22.UpTable)),
   881  	}
   882  	expectedLock[lockID2].Unsynced = []string{}
   883  	checkLocksByMap(t.T(), o, expectedLock, []string{}, lockID1, lockID2)
   884  
   885  	// wait operation for i12 become available.
   886  	opCh := make(chan optimism.Operation, 10)
   887  	errCh := make(chan error, 10)
   888  	var op12 optimism.Operation
   889  	ctx2, cancel2 := context.WithCancel(ctx)
   890  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i12.Task, i12.Source, i12.UpSchema, i12.UpTable, rev1, opCh, errCh)
   891  	select {
   892  	case <-time.After(watchTimeout):
   893  		t.T().Fatal("timeout")
   894  	case op12 = <-opCh:
   895  		require.Equal(t.T(), DDLs, op12.DDLs)
   896  		require.Equal(t.T(), optimism.ConflictNone, op12.ConflictStage)
   897  	}
   898  	cancel2()
   899  	close(opCh)
   900  	close(errCh)
   901  	require.Equal(t.T(), 0, len(opCh))
   902  	require.Equal(t.T(), 0, len(errCh))
   903  
   904  	// mark op11 and op12 as done, the lock should be resolved.
   905  	op11c := op12
   906  	op11c.Done = true
   907  	op11c.UpTable = i11.UpTable // overwrite `UpTable`.
   908  	_, putted, err := optimism.PutOperation(t.etcdTestCli, false, op11c, 0)
   909  	require.NoError(t.T(), err)
   910  	require.True(t.T(), putted)
   911  	op12c := op12
   912  	op12c.Done = true
   913  	_, putted, err = optimism.PutOperation(t.etcdTestCli, false, op12c, 0)
   914  	require.NoError(t.T(), err)
   915  	require.True(t.T(), putted)
   916  	require.Eventually(t.T(), func() bool {
   917  		_, ok := o.Locks()[lockID1]
   918  		return !ok
   919  	}, waitFor, tick)
   920  	require.Len(t.T(), o.Locks(), 1)
   921  	checkLocksByMap(t.T(), o, expectedLock, nil, lockID2)
   922  
   923  	// wait operation for i22 become available.
   924  	opCh = make(chan optimism.Operation, 10)
   925  	errCh = make(chan error, 10)
   926  	var op22 optimism.Operation
   927  	ctx2, cancel2 = context.WithCancel(ctx)
   928  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i22.Task, i22.Source, i22.UpSchema, i22.UpTable, rev2, opCh, errCh)
   929  	select {
   930  	case <-time.After(watchTimeout):
   931  		t.T().Fatal("timeout")
   932  	case op22 = <-opCh:
   933  		require.Equal(t.T(), DDLs, op22.DDLs)
   934  		require.Equal(t.T(), optimism.ConflictNone, op22.ConflictStage)
   935  	}
   936  	cancel2()
   937  	close(opCh)
   938  	close(errCh)
   939  	require.Equal(t.T(), 0, len(opCh))
   940  	require.Equal(t.T(), 0, len(errCh))
   941  
   942  	// mark op21 and op22 as done, the lock should be resolved.
   943  	op21c := op22
   944  	op21c.Done = true
   945  	op21c.UpTable = i21.UpTable // overwrite `UpTable`.
   946  	_, putted, err = optimism.PutOperation(t.etcdTestCli, false, op21c, 0)
   947  	require.NoError(t.T(), err)
   948  	require.True(t.T(), putted)
   949  	op22c := op22
   950  	op22c.Done = true
   951  	_, putted, err = optimism.PutOperation(t.etcdTestCli, false, op22c, 0)
   952  	require.NoError(t.T(), err)
   953  	require.True(t.T(), putted)
   954  	require.Eventually(t.T(), func() bool {
   955  		_, ok := o.Locks()[lockID2]
   956  		return !ok
   957  	}, waitFor, tick)
   958  	require.Len(t.T(), o.Locks(), 0)
   959  	checkLocksByMap(t.T(), o, expectedLock, nil)
   960  }
   961  
   962  func (t *testOptimistSuite) TestOptimistInitSchema() {
   963  	var (
   964  		tick         = 100 * time.Millisecond
   965  		waitFor      = 30 * tick
   966  		watchTimeout = 5 * time.Second
   967  		logger       = log.L()
   968  		o            = NewOptimist(&logger, getDownstreamMeta)
   969  		task         = "test-optimist-init-schema"
   970  		source       = "mysql-replica-1"
   971  		upSchema     = "foo"
   972  		upTables     = []string{"bar-1", "bar-2"}
   973  		downSchema   = "foo"
   974  		downTable    = "bar"
   975  		st           = optimism.NewSourceTables(task, source)
   976  
   977  		p           = parser.New()
   978  		se          = mock.NewContext()
   979  		tblID int64 = 111
   980  		DDLs1       = []string{"ALTER TABLE bar ADD COLUMN c1 TEXT"}
   981  		DDLs2       = []string{"ALTER TABLE bar ADD COLUMN c2 INT"}
   982  		ti0         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
   983  		ti1         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT)`)
   984  		ti2         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT, c2 INT)`)
   985  		i11         = optimism.NewInfo(task, source, upSchema, upTables[0], downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
   986  		i12         = optimism.NewInfo(task, source, upSchema, upTables[1], downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
   987  		i21         = optimism.NewInfo(task, source, upSchema, upTables[0], downSchema, downTable, DDLs2, ti1, []*model.TableInfo{ti2})
   988  	)
   989  
   990  	st.AddTable(upSchema, upTables[0], downSchema, downTable)
   991  	st.AddTable(upSchema, upTables[1], downSchema, downTable)
   992  
   993  	// put source tables first.
   994  	_, err := optimism.PutSourceTables(t.etcdTestCli, st)
   995  	require.NoError(t.T(), err)
   996  
   997  	ctx, cancel := context.WithCancel(context.Background())
   998  	defer func() {
   999  		cancel()
  1000  		o.Close()
  1001  	}()
  1002  
  1003  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
  1004  	require.Len(t.T(), o.Locks(), 0)
  1005  
  1006  	// PUT i11, will creat a lock.
  1007  	_, err = optimism.PutInfo(t.etcdTestCli, i11)
  1008  	require.NoError(t.T(), err)
  1009  	require.Eventually(t.T(), func() bool {
  1010  		return len(o.Locks()) == 1
  1011  	}, waitFor, tick)
  1012  	time.Sleep(tick) // sleep one more time to wait for update of init schema.
  1013  
  1014  	// PUT i12, the lock will be synced.
  1015  	rev1, err := optimism.PutInfo(t.etcdTestCli, i12)
  1016  	require.NoError(t.T(), err)
  1017  
  1018  	// wait operation for i12 become available.
  1019  	opCh := make(chan optimism.Operation, 10)
  1020  	errCh := make(chan error, 10)
  1021  	var op12 optimism.Operation
  1022  	ctx2, cancel2 := context.WithCancel(ctx)
  1023  	go optimism.WatchOperationPut(ctx2, t.etcdTestCli, i12.Task, i12.Source, i12.UpSchema, i12.UpTable, rev1, opCh, errCh)
  1024  	select {
  1025  	case <-time.After(watchTimeout):
  1026  		t.T().Fatal("timeout")
  1027  	case op12 = <-opCh:
  1028  		require.Equal(t.T(), DDLs1, op12.DDLs)
  1029  		require.Equal(t.T(), optimism.ConflictNone, op12.ConflictStage)
  1030  	}
  1031  	cancel2()
  1032  	close(opCh)
  1033  	close(errCh)
  1034  	require.Equal(t.T(), 0, len(opCh))
  1035  	require.Equal(t.T(), 0, len(errCh))
  1036  
  1037  	// mark op11 and op12 as done, the lock should be resolved.
  1038  	op11c := op12
  1039  	op11c.Done = true
  1040  	op11c.UpTable = i11.UpTable // overwrite `UpTable`.
  1041  	_, putted, err := optimism.PutOperation(t.etcdTestCli, false, op11c, 0)
  1042  	require.NoError(t.T(), err)
  1043  	require.True(t.T(), putted)
  1044  	op12c := op12
  1045  	op12c.Done = true
  1046  	_, putted, err = optimism.PutOperation(t.etcdTestCli, false, op12c, 0)
  1047  	require.NoError(t.T(), err)
  1048  	require.True(t.T(), putted)
  1049  	require.Eventually(t.T(), func() bool {
  1050  		return len(o.Locks()) == 0
  1051  	}, waitFor, tick)
  1052  
  1053  	// PUT i21 to create the lock again.
  1054  	_, err = optimism.PutInfo(t.etcdTestCli, i21)
  1055  	require.NoError(t.T(), err)
  1056  	require.Eventually(t.T(), func() bool {
  1057  		return len(o.Locks()) == 1
  1058  	}, waitFor, tick)
  1059  	time.Sleep(tick) // sleep one more time to wait for update of init schema.
  1060  }
  1061  
  1062  func (t *testOptimistSuite) testSortInfos(cli *clientv3.Client) {
  1063  	defer func() {
  1064  		require.NoError(t.T(), optimism.ClearTestInfoOperationColumn(cli))
  1065  	}()
  1066  
  1067  	var (
  1068  		task       = "test-optimist-init-schema"
  1069  		sources    = []string{"mysql-replica-1", "mysql-replica-2"}
  1070  		upSchema   = "foo"
  1071  		upTables   = []string{"bar-1", "bar-2"}
  1072  		downSchema = "foo"
  1073  		downTable  = "bar"
  1074  
  1075  		p           = parser.New()
  1076  		se          = mock.NewContext()
  1077  		tblID int64 = 111
  1078  		DDLs1       = []string{"ALTER TABLE bar ADD COLUMN c1 TEXT"}
  1079  		DDLs2       = []string{"ALTER TABLE bar ADD COLUMN c2 INT"}
  1080  		ti0         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
  1081  		ti1         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT)`)
  1082  		ti2         = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 TEXT, c2 INT)`)
  1083  		i11         = optimism.NewInfo(task, sources[0], upSchema, upTables[0], downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
  1084  		i12         = optimism.NewInfo(task, sources[0], upSchema, upTables[1], downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
  1085  		i21         = optimism.NewInfo(task, sources[1], upSchema, upTables[1], downSchema, downTable, DDLs2, ti1, []*model.TableInfo{ti2})
  1086  	)
  1087  
  1088  	rev1, err := optimism.PutInfo(cli, i11)
  1089  	require.NoError(t.T(), err)
  1090  	ifm, _, err := optimism.GetAllInfo(cli)
  1091  	require.NoError(t.T(), err)
  1092  	infos := sortInfos(ifm)
  1093  	require.Equal(t.T(), 1, len(infos))
  1094  	i11.Version = 1
  1095  	i11.Revision = rev1
  1096  	require.Equal(t.T(), i11, infos[0])
  1097  
  1098  	rev2, err := optimism.PutInfo(cli, i12)
  1099  	require.NoError(t.T(), err)
  1100  	ifm, _, err = optimism.GetAllInfo(cli)
  1101  	require.NoError(t.T(), err)
  1102  	infos = sortInfos(ifm)
  1103  	require.Equal(t.T(), 2, len(infos))
  1104  	i11.Version = 1
  1105  	i11.Revision = rev1
  1106  	i12.Version = 1
  1107  	i12.Revision = rev2
  1108  	require.Equal(t.T(), i11, infos[0])
  1109  	require.Equal(t.T(), i12, infos[1])
  1110  
  1111  	rev3, err := optimism.PutInfo(cli, i21)
  1112  	require.NoError(t.T(), err)
  1113  	rev4, err := optimism.PutInfo(cli, i11)
  1114  	require.NoError(t.T(), err)
  1115  	ifm, _, err = optimism.GetAllInfo(cli)
  1116  	require.NoError(t.T(), err)
  1117  	infos = sortInfos(ifm)
  1118  	require.Equal(t.T(), 3, len(infos))
  1119  
  1120  	i11.Version = 2
  1121  	i11.Revision = rev4
  1122  	i12.Version = 1
  1123  	i12.Revision = rev2
  1124  	i21.Version = 1
  1125  	i21.Revision = rev3
  1126  	require.Equal(t.T(), i12, infos[0])
  1127  	require.Equal(t.T(), i21, infos[1])
  1128  	require.Equal(t.T(), i11, infos[2])
  1129  }
  1130  
  1131  func (t *testOptimistSuite) TestBuildLockJoinedAndTable() {
  1132  	var (
  1133  		logger           = log.L()
  1134  		o                = NewOptimist(&logger, getDownstreamMeta)
  1135  		task             = "task-build-lock-joined-and-table"
  1136  		source1          = "mysql-replica-1"
  1137  		source2          = "mysql-replica-2"
  1138  		downSchema       = "db"
  1139  		downTable        = "tbl"
  1140  		st1              = optimism.NewSourceTables(task, source1)
  1141  		st2              = optimism.NewSourceTables(task, source2)
  1142  		DDLs1            = []string{"ALTER TABLE bar ADD COLUMN c1 INT"}
  1143  		DDLs2            = []string{"ALTER TABLE bar DROP COLUMN c1"}
  1144  		p                = parser.New()
  1145  		se               = mock.NewContext()
  1146  		tblID      int64 = 111
  1147  		ti0              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY)`)
  1148  		ti1              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 INT)`)
  1149  		ti2              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c1 INT, c2 INT)`)
  1150  		ti3              = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (id INT PRIMARY KEY, c2 INT)`)
  1151  
  1152  		i11 = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, DDLs1, ti0, []*model.TableInfo{ti1})
  1153  		i21 = optimism.NewInfo(task, source2, "foo", "bar-1", downSchema, downTable, DDLs2, ti2, []*model.TableInfo{ti3})
  1154  	)
  1155  
  1156  	ctx, cancel := context.WithCancel(context.Background())
  1157  	defer func() {
  1158  		cancel()
  1159  		o.Close()
  1160  	}()
  1161  
  1162  	st1.AddTable("foo", "bar-1", downSchema, downTable)
  1163  	st2.AddTable("foo", "bar-1", downSchema, downTable)
  1164  
  1165  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
  1166  	_, err := optimism.PutSourceTables(t.etcdTestCli, st1)
  1167  	require.NoError(t.T(), err)
  1168  	_, err = optimism.PutSourceTables(t.etcdTestCli, st2)
  1169  	require.NoError(t.T(), err)
  1170  
  1171  	_, err = optimism.PutInfo(t.etcdTestCli, i21)
  1172  	require.NoError(t.T(), err)
  1173  	_, err = optimism.PutInfo(t.etcdTestCli, i11)
  1174  	require.NoError(t.T(), err)
  1175  
  1176  	stm, _, err := optimism.GetAllSourceTables(t.etcdTestCli)
  1177  	require.NoError(t.T(), err)
  1178  	o.tk.Init(stm)
  1179  }
  1180  
  1181  func (t *testOptimistSuite) TestBuildLockWithInitSchema() {
  1182  	var (
  1183  		logger     = log.L()
  1184  		o          = NewOptimist(&logger, getDownstreamMeta)
  1185  		task       = "task-lock-with-init-schema"
  1186  		source1    = "mysql-replica-1"
  1187  		source2    = "mysql-replica-2"
  1188  		downSchema = "db"
  1189  		downTable  = "tbl"
  1190  		st1        = optimism.NewSourceTables(task, source1)
  1191  		st2        = optimism.NewSourceTables(task, source2)
  1192  		p          = parser.New()
  1193  		se         = mock.NewContext()
  1194  		tblID      = int64(111)
  1195  
  1196  		ti0 = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (a INT PRIMARY KEY, b INT, c INT)`)
  1197  		ti1 = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (a INT PRIMARY KEY, b INT)`)
  1198  		ti2 = createTableInfo(t.T(), p, se, tblID, `CREATE TABLE bar (a INT PRIMARY KEY)`)
  1199  
  1200  		ddlDropB  = "ALTER TABLE bar DROP COLUMN b"
  1201  		ddlDropC  = "ALTER TABLE bar DROP COLUMN c"
  1202  		infoDropB = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, []string{ddlDropC}, ti0, []*model.TableInfo{ti1})
  1203  		infoDropC = optimism.NewInfo(task, source1, "foo", "bar-1", downSchema, downTable, []string{ddlDropB}, ti1, []*model.TableInfo{ti2})
  1204  	)
  1205  
  1206  	ctx, cancel := context.WithCancel(context.Background())
  1207  	defer func() {
  1208  		cancel()
  1209  		o.Close()
  1210  	}()
  1211  
  1212  	st1.AddTable("foo", "bar-1", downSchema, downTable)
  1213  	st2.AddTable("foo", "bar-1", downSchema, downTable)
  1214  
  1215  	require.NoError(t.T(), o.Start(ctx, t.etcdTestCli))
  1216  	_, err := optimism.PutSourceTables(t.etcdTestCli, st1)
  1217  	require.NoError(t.T(), err)
  1218  	_, err = optimism.PutSourceTables(t.etcdTestCli, st2)
  1219  	require.NoError(t.T(), err)
  1220  
  1221  	_, err = optimism.PutInfo(t.etcdTestCli, infoDropB)
  1222  	require.NoError(t.T(), err)
  1223  	_, err = optimism.PutInfo(t.etcdTestCli, infoDropC)
  1224  	require.NoError(t.T(), err)
  1225  
  1226  	stm, _, err := optimism.GetAllSourceTables(t.etcdTestCli)
  1227  	require.NoError(t.T(), err)
  1228  	o.tk.Init(stm)
  1229  }
  1230  
  1231  func getDownstreamMeta(string) (*dbconfig.DBConfig, string) {
  1232  	return nil, ""
  1233  }