github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/etcd/etcd_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 etcd
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"sort"
    20  	"sync"
    21  	"sync/atomic"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/pingcap/tiflow/cdc/model"
    26  	"github.com/pingcap/tiflow/pkg/config"
    27  	cerror "github.com/pingcap/tiflow/pkg/errors"
    28  	"github.com/stretchr/testify/require"
    29  	clientv3 "go.etcd.io/etcd/client/v3"
    30  	"go.etcd.io/etcd/client/v3/concurrency"
    31  )
    32  
    33  type Captures []*model.CaptureInfo
    34  
    35  func (c Captures) Len() int           { return len(c) }
    36  func (c Captures) Less(i, j int) bool { return c[i].ID < c[j].ID }
    37  func (c Captures) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
    38  
    39  func TestEmbedEtcd(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	s := &Tester{}
    43  	s.SetUpTest(t)
    44  	defer s.TearDownTest(t)
    45  	curl := s.ClientURL.String()
    46  	cli, err := clientv3.New(clientv3.Config{
    47  		Endpoints:   []string{curl},
    48  		DialTimeout: 3 * time.Second,
    49  	})
    50  	require.NoError(t, err)
    51  	defer cli.Close()
    52  
    53  	var (
    54  		key = "test-key"
    55  		val = "test-val"
    56  	)
    57  	_, err = cli.Put(context.Background(), key, val)
    58  	require.NoError(t, err)
    59  	resp, err2 := cli.Get(context.Background(), key)
    60  	require.NoError(t, err2)
    61  	require.Len(t, resp.Kvs, 1)
    62  	require.Equal(t, resp.Kvs[0].Value, []byte(val))
    63  }
    64  
    65  func TestGetChangeFeeds(t *testing.T) {
    66  	t.Parallel()
    67  
    68  	s := &Tester{}
    69  	s.SetUpTest(t)
    70  	defer s.TearDownTest(t)
    71  	testCases := []struct {
    72  		ids     []string
    73  		details []string
    74  	}{
    75  		{ids: nil, details: nil},
    76  		{ids: []string{"id"}, details: []string{"detail"}},
    77  		{ids: []string{"id", "id1", "id2"}, details: []string{"detail", "detail1", "detail2"}},
    78  	}
    79  	for _, tc := range testCases {
    80  		for i := 0; i < len(tc.ids); i++ {
    81  			_, err := s.client.GetEtcdClient().Put(context.Background(),
    82  				GetEtcdKeyChangeFeedInfo(DefaultCDCClusterID,
    83  					model.DefaultChangeFeedID(tc.ids[i])),
    84  				tc.details[i])
    85  			require.NoError(t, err)
    86  		}
    87  		_, result, err := s.client.GetChangeFeeds(context.Background())
    88  		require.NoError(t, err)
    89  		require.NoError(t, err)
    90  		require.Equal(t, len(result), len(tc.ids))
    91  		for i := 0; i < len(tc.ids); i++ {
    92  			rawKv, ok := result[model.DefaultChangeFeedID(tc.ids[i])]
    93  			require.True(t, ok)
    94  			require.Equal(t, string(rawKv.Value), tc.details[i])
    95  		}
    96  	}
    97  	_, result, err := s.client.GetChangeFeeds(context.Background())
    98  	require.NoError(t, err)
    99  	require.Equal(t, len(result), 3)
   100  
   101  	err = s.client.ClearAllCDCInfo(context.Background())
   102  	require.NoError(t, err)
   103  
   104  	_, result, err = s.client.GetChangeFeeds(context.Background())
   105  	require.NoError(t, err)
   106  	require.Equal(t, len(result), 0)
   107  }
   108  
   109  func TestOpChangeFeedDetail(t *testing.T) {
   110  	t.Parallel()
   111  
   112  	s := &Tester{}
   113  	s.SetUpTest(t)
   114  	defer s.TearDownTest(t)
   115  	ctx := context.Background()
   116  	detail := &model.ChangeFeedInfo{
   117  		SinkURI: "root@tcp(127.0.0.1:3306)/mysql",
   118  		SortDir: "/old-version/sorter",
   119  	}
   120  	cfID := model.DefaultChangeFeedID("test-op-cf")
   121  
   122  	err := s.client.SaveChangeFeedInfo(ctx, detail, cfID)
   123  	require.NoError(t, err)
   124  
   125  	d, err := s.client.GetChangeFeedInfo(ctx, cfID)
   126  	require.NoError(t, err)
   127  	require.Equal(t, d.SinkURI, detail.SinkURI)
   128  	require.Equal(t, d.SortDir, detail.SortDir)
   129  
   130  	err = s.client.DeleteChangeFeedInfo(ctx, cfID)
   131  	require.NoError(t, err)
   132  
   133  	_, err = s.client.GetChangeFeedInfo(ctx, cfID)
   134  	require.True(t, cerror.ErrChangeFeedNotExists.Equal(err))
   135  }
   136  
   137  func TestGetAllChangeFeedInfo(t *testing.T) {
   138  	t.Parallel()
   139  
   140  	s := &Tester{}
   141  	s.SetUpTest(t)
   142  	defer s.TearDownTest(t)
   143  	ctx := context.Background()
   144  	infos := []struct {
   145  		id   string
   146  		info *model.ChangeFeedInfo
   147  	}{
   148  		{
   149  			id: "a",
   150  			info: &model.ChangeFeedInfo{
   151  				SinkURI: "root@tcp(127.0.0.1:3306)/mysql",
   152  				SortDir: "/old-version/sorter",
   153  			},
   154  		},
   155  		{
   156  			id: "b",
   157  			info: &model.ChangeFeedInfo{
   158  				SinkURI: "root@tcp(127.0.0.1:4000)/mysql",
   159  			},
   160  		},
   161  	}
   162  
   163  	for _, item := range infos {
   164  		err := s.client.SaveChangeFeedInfo(ctx,
   165  			item.info,
   166  			model.DefaultChangeFeedID(item.id))
   167  		require.NoError(t, err)
   168  	}
   169  
   170  	allChangFeedInfo, err := s.client.GetAllChangeFeedInfo(ctx)
   171  	require.NoError(t, err)
   172  
   173  	for _, item := range infos {
   174  		obtained, found := allChangFeedInfo[model.DefaultChangeFeedID(item.id)]
   175  		require.True(t, found)
   176  		require.Equal(t, item.info.SinkURI, obtained.SinkURI)
   177  		require.Equal(t, item.info.SortDir, obtained.SortDir)
   178  	}
   179  }
   180  
   181  func TestCheckMultipleCDCClusterExist(t *testing.T) {
   182  	t.Parallel()
   183  
   184  	s := &Tester{}
   185  	s.SetUpTest(t)
   186  	defer s.TearDownTest(t)
   187  
   188  	ctx := context.Background()
   189  	rawEtcdClient := s.client.GetEtcdClient().cli
   190  	defaultClusterKey := DefaultClusterAndNamespacePrefix + "/test-key"
   191  	_, err := rawEtcdClient.Put(ctx, defaultClusterKey, "test-value")
   192  	require.NoError(t, err)
   193  
   194  	err = s.client.CheckMultipleCDCClusterExist(ctx)
   195  	require.NoError(t, err)
   196  
   197  	for _, reserved := range config.ReservedClusterIDs {
   198  		newClusterKey := "/tidb/cdc/" + reserved
   199  		_, err = rawEtcdClient.Put(ctx, newClusterKey, "test-value")
   200  		require.NoError(t, err)
   201  		err = s.client.CheckMultipleCDCClusterExist(ctx)
   202  		require.NoError(t, err)
   203  	}
   204  
   205  	newClusterKey := NamespacedPrefix("new-cluster", "new-namespace") +
   206  		"/test-key"
   207  	_, err = rawEtcdClient.Put(ctx, newClusterKey, "test-value")
   208  	require.NoError(t, err)
   209  
   210  	err = s.client.CheckMultipleCDCClusterExist(ctx)
   211  	require.Error(t, err)
   212  	require.Contains(t, err.Error(), "ErrMultipleCDCClustersExist")
   213  }
   214  
   215  func TestCreateChangefeed(t *testing.T) {
   216  	t.Parallel()
   217  
   218  	s := &Tester{}
   219  	s.SetUpTest(t)
   220  	defer s.TearDownTest(t)
   221  
   222  	ctx := context.Background()
   223  	detail := &model.ChangeFeedInfo{
   224  		UpstreamID: 1,
   225  		Namespace:  "test",
   226  		ID:         "create-changefeed",
   227  		SinkURI:    "root@tcp(127.0.0.1:3306)/mysql",
   228  	}
   229  
   230  	upstreamInfo := &model.UpstreamInfo{ID: 1}
   231  	err := s.client.CreateChangefeedInfo(ctx,
   232  		upstreamInfo, detail)
   233  	require.NoError(t, err)
   234  
   235  	err = s.client.CreateChangefeedInfo(ctx,
   236  		upstreamInfo, detail)
   237  	require.True(t, cerror.ErrMetaOpFailed.Equal(err))
   238  	require.Equal(t, "[DFLOW:ErrMetaOpFailed]unexpected meta operation failure: Create changefeed test/create-changefeed", err.Error())
   239  }
   240  
   241  func TestUpdateChangefeedAndUpstream(t *testing.T) {
   242  	t.Parallel()
   243  
   244  	s := &Tester{}
   245  	s.SetUpTest(t)
   246  	defer s.TearDownTest(t)
   247  
   248  	ctx := context.Background()
   249  	upstreamInfo := &model.UpstreamInfo{
   250  		ID:          1,
   251  		PDEndpoints: "http://127.0.0.1:2385",
   252  	}
   253  	changeFeedID := model.DefaultChangeFeedID("test-update-cf-and-up")
   254  	changeFeedInfo := &model.ChangeFeedInfo{
   255  		UpstreamID: upstreamInfo.ID,
   256  		ID:         changeFeedID.ID,
   257  		Namespace:  changeFeedID.Namespace,
   258  		SinkURI:    "blackhole://",
   259  	}
   260  
   261  	err := s.client.SaveChangeFeedInfo(ctx, changeFeedInfo, changeFeedID)
   262  	require.NoError(t, err)
   263  
   264  	err = s.client.UpdateChangefeedAndUpstream(ctx, upstreamInfo, changeFeedInfo)
   265  	require.NoError(t, err)
   266  
   267  	var upstreamResult *model.UpstreamInfo
   268  	var changefeedResult *model.ChangeFeedInfo
   269  
   270  	upstreamResult, err = s.client.GetUpstreamInfo(ctx, 1, changeFeedID.Namespace)
   271  	require.NoError(t, err)
   272  	require.Equal(t, upstreamInfo.PDEndpoints, upstreamResult.PDEndpoints)
   273  
   274  	changefeedResult, err = s.client.GetChangeFeedInfo(ctx, changeFeedID)
   275  	require.NoError(t, err)
   276  	require.Equal(t, changeFeedInfo.SinkURI, changefeedResult.SinkURI)
   277  }
   278  
   279  func TestGetAllCaptureLeases(t *testing.T) {
   280  	t.Parallel()
   281  
   282  	s := &Tester{}
   283  	s.SetUpTest(t)
   284  	defer s.TearDownTest(t)
   285  
   286  	ctx, cancel := context.WithCancel(context.Background())
   287  	defer cancel()
   288  	testCases := []*model.CaptureInfo{
   289  		{
   290  			ID:            "a3f41a6a-3c31-44f4-aa27-344c1b8cd658",
   291  			AdvertiseAddr: "127.0.0.1:8301",
   292  		},
   293  		{
   294  			ID:            "cdb041d9-ccdd-480d-9975-e97d7adb1185",
   295  			AdvertiseAddr: "127.0.0.1:8302",
   296  		},
   297  		{
   298  			ID:            "e05e5d34-96ea-44af-812d-ca72aa19e1e5",
   299  			AdvertiseAddr: "127.0.0.1:8303",
   300  		},
   301  	}
   302  	leases := make(map[string]int64)
   303  
   304  	for _, cinfo := range testCases {
   305  		sess, err := concurrency.NewSession(s.client.GetEtcdClient().Unwrap(),
   306  			concurrency.WithTTL(10), concurrency.WithContext(ctx))
   307  		require.NoError(t, err)
   308  		err = s.client.PutCaptureInfo(ctx, cinfo, sess.Lease())
   309  		require.NoError(t, err)
   310  		leases[cinfo.ID] = int64(sess.Lease())
   311  	}
   312  
   313  	_, captures, err := s.client.GetCaptures(ctx)
   314  	require.NoError(t, err)
   315  	require.Len(t, captures, len(testCases))
   316  	sort.Sort(Captures(captures))
   317  	require.Equal(t, captures, testCases)
   318  
   319  	queryLeases, err := s.client.GetCaptureLeases(ctx)
   320  	require.NoError(t, err)
   321  	require.Equal(t, queryLeases, leases)
   322  
   323  	// make sure the RevokeAllLeases function can ignore the lease not exist
   324  	leases["/fake/capture/info"] = 200
   325  	err = s.client.RevokeAllLeases(ctx, leases)
   326  	require.NoError(t, err)
   327  	queryLeases, err = s.client.GetCaptureLeases(ctx)
   328  	require.NoError(t, err)
   329  	require.Equal(t, queryLeases, map[string]int64{})
   330  }
   331  
   332  const (
   333  	testOwnerRevisionForMaxEpochs = 16
   334  )
   335  
   336  func TestGetOwnerRevision(t *testing.T) {
   337  	t.Parallel()
   338  
   339  	s := &Tester{}
   340  	s.SetUpTest(t)
   341  	defer s.TearDownTest(t)
   342  
   343  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   344  	defer cancel()
   345  
   346  	// First we check that GetOwnerRevision correctly reports errors
   347  	// Note that there is no owner for now.
   348  	_, err := s.client.GetOwnerRevision(ctx, "fake-capture-id")
   349  	require.Contains(t, err.Error(), "ErrOwnerNotFound")
   350  
   351  	var (
   352  		ownerRev int64
   353  		epoch    int32
   354  		wg       sync.WaitGroup
   355  	)
   356  
   357  	// We will create 3 mock captures, and they will become the owner one by one.
   358  	// While each is the owner, it tries to get its owner revision, and
   359  	// checks that the global monotonicity is guaranteed.
   360  
   361  	wg.Add(3)
   362  	for i := 0; i < 3; i++ {
   363  		i := i
   364  		go func() {
   365  			defer wg.Done()
   366  			sess, err := concurrency.NewSession(s.client.GetEtcdClient().Unwrap(),
   367  				concurrency.WithTTL(10 /* seconds */))
   368  			require.Nil(t, err)
   369  			election := concurrency.NewElection(sess,
   370  				CaptureOwnerKey(DefaultCDCClusterID))
   371  
   372  			mockCaptureID := fmt.Sprintf("capture-%d", i)
   373  
   374  			for {
   375  				err = election.Campaign(ctx, mockCaptureID)
   376  				if err != nil {
   377  					require.Contains(t, err.Error(), "context canceled")
   378  					return
   379  				}
   380  
   381  				rev, err := s.client.GetOwnerRevision(ctx, mockCaptureID)
   382  				require.NoError(t, err)
   383  
   384  				_, err = s.client.GetOwnerRevision(ctx, "fake-capture-id")
   385  				require.Contains(t, err.Error(), "ErrNotOwner")
   386  
   387  				lastRev := atomic.SwapInt64(&ownerRev, rev)
   388  				require.Less(t, lastRev, rev)
   389  
   390  				err = election.Resign(ctx)
   391  				if err != nil {
   392  					require.Contains(t, err.Error(), "context canceled")
   393  					return
   394  				}
   395  
   396  				if atomic.AddInt32(&epoch, 1) >= testOwnerRevisionForMaxEpochs {
   397  					return
   398  				}
   399  			}
   400  		}()
   401  	}
   402  
   403  	wg.Wait()
   404  }
   405  
   406  func TestExtractKeySuffix(t *testing.T) {
   407  	t.Parallel()
   408  
   409  	testCases := []struct {
   410  		input  string
   411  		expect string
   412  		hasErr bool
   413  	}{
   414  		{"/tidb/cdc/capture/info/6a6c6dd290bc8732", "6a6c6dd290bc8732", false},
   415  		{"/tidb/cdc/capture/info/6a6c6dd290bc8732/", "", false},
   416  		{"/tidb/cdc", "cdc", false},
   417  		{"/tidb", "tidb", false},
   418  		{"", "", true},
   419  	}
   420  	for _, tc := range testCases {
   421  		key, err := extractKeySuffix(tc.input)
   422  		if tc.hasErr {
   423  			require.NotNil(t, err)
   424  		} else {
   425  			require.Nil(t, err)
   426  			require.Equal(t, tc.expect, key)
   427  		}
   428  	}
   429  }
   430  
   431  func TestMigrateBackupKey(t *testing.T) {
   432  	t.Parallel()
   433  
   434  	key := MigrateBackupKey(1, "/tidb/cdc/capture/abcd")
   435  	require.Equal(t, "/tidb/cdc/__backup__/1/tidb/cdc/capture/abcd", key)
   436  	key = MigrateBackupKey(1, "abcdc")
   437  	require.Equal(t, "/tidb/cdc/__backup__/1/abcdc", key)
   438  }
   439  
   440  func TestDeleteCaptureInfo(t *testing.T) {
   441  	t.Parallel()
   442  
   443  	s := &Tester{}
   444  	s.SetUpTest(t)
   445  	defer s.TearDownTest(t)
   446  
   447  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   448  	defer cancel()
   449  	captureID := "test-capture-id"
   450  
   451  	changefeedStatus := map[model.ChangeFeedID]model.ChangeFeedStatus{
   452  		model.DefaultChangeFeedID("test-cf-1"): {CheckpointTs: 1},
   453  	}
   454  
   455  	for id, status := range changefeedStatus {
   456  		val, err := status.Marshal()
   457  		require.NoError(t, err)
   458  		statusKey := fmt.Sprintf("%s/%s", ChangefeedStatusKeyPrefix(DefaultCDCClusterID, id.Namespace), id.ID)
   459  		_, err = s.client.Client.Put(ctx, statusKey, val)
   460  		require.NoError(t, err)
   461  
   462  		_, err = s.client.Client.Put(
   463  			ctx, GetEtcdKeyTaskPosition(DefaultCDCClusterID, id, captureID),
   464  			fmt.Sprintf("task-%s", id.ID))
   465  		require.NoError(t, err)
   466  	}
   467  	err := s.client.DeleteCaptureInfo(ctx, captureID)
   468  	require.NoError(t, err)
   469  	for id := range changefeedStatus {
   470  		taskPositionKey := GetEtcdKeyTaskPosition(DefaultCDCClusterID, id, captureID)
   471  		v, err := s.client.Client.Get(ctx, taskPositionKey)
   472  		require.NoError(t, err)
   473  		require.Equal(t, 0, len(v.Kvs))
   474  	}
   475  }