github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/etcdutil/etcdutil_test.go (about)

     1  // Copyright 2019 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 etcdutil
    15  
    16  import (
    17  	"fmt"
    18  	"net/url"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	. "github.com/pingcap/check"
    24  	"github.com/pingcap/failpoint"
    25  	"github.com/pingcap/tiflow/dm/pkg/log"
    26  	"github.com/pingcap/tiflow/dm/pkg/terror"
    27  	"github.com/stretchr/testify/require"
    28  	"github.com/tikv/pd/pkg/utils/tempurl"
    29  	"go.etcd.io/etcd/api/v3/etcdserverpb"
    30  	v3rpc "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
    31  	clientv3 "go.etcd.io/etcd/client/v3"
    32  	"go.etcd.io/etcd/client/v3/clientv3util"
    33  	"go.etcd.io/etcd/server/v3/embed"
    34  	"go.etcd.io/etcd/tests/v3/integration"
    35  )
    36  
    37  var etcdTestSuite = Suite(&testEtcdUtilSuite{})
    38  
    39  type testEtcdUtilSuite struct {
    40  	testT *testing.T
    41  }
    42  
    43  func (t *testEtcdUtilSuite) SetUpSuite(c *C) {
    44  	// initialized the logger to make genEmbedEtcdConfig working.
    45  	c.Assert(log.InitLogger(&log.Config{}), IsNil)
    46  	// this is to trigger  `etcd.io/etcd/embed.(*Config).setupLogging()`
    47  	// otherwise `newConfig` will datarace with `TestDoOpsInOneTxnWithRetry.NewClusterV3.Launch.m.grpcServer.Serve(m.grpcListener)`
    48  	t.newConfig(c, "not used", 1)
    49  }
    50  
    51  func TestSuite(t *testing.T) {
    52  	integration.BeforeTestExternal(t)
    53  	// inject *testing.T to suite
    54  	s := etcdTestSuite.(*testEtcdUtilSuite)
    55  	s.testT = t
    56  	TestingT(t)
    57  }
    58  
    59  func (t *testEtcdUtilSuite) newConfig(c *C, name string, portCount int) *embed.Config {
    60  	cfg := embed.NewConfig()
    61  	cfg.Name = name
    62  	cfg.Dir = c.MkDir()
    63  	cfg.ZapLoggerBuilder = embed.NewZapCoreLoggerBuilder(log.L().Logger, log.L().Core(), log.Props().Syncer)
    64  	cfg.Logger = "zap"
    65  	c.Assert(cfg.Validate(), IsNil)
    66  
    67  	cfg.ListenClientUrls = []url.URL{}
    68  	for i := 0; i < portCount; i++ {
    69  		endPoint := tempurl.Alloc()
    70  		cu, err2 := url.Parse(endPoint)
    71  		c.Assert(err2, IsNil)
    72  		cfg.ListenClientUrls = append(cfg.ListenClientUrls, *cu)
    73  	}
    74  
    75  	cfg.AdvertiseClientUrls = cfg.ListenClientUrls
    76  	cfg.ListenPeerUrls = []url.URL{}
    77  	ic := make([]string, 0, portCount)
    78  	for i := 0; i < portCount; i++ {
    79  		endPoint := tempurl.Alloc()
    80  		pu, err2 := url.Parse(endPoint)
    81  		c.Assert(err2, IsNil)
    82  		cfg.ListenPeerUrls = append(cfg.ListenPeerUrls, *pu)
    83  		ic = append(ic, fmt.Sprintf("%s=%s", cfg.Name, pu))
    84  	}
    85  	cfg.AdvertisePeerUrls = cfg.ListenPeerUrls
    86  	cfg.InitialCluster = strings.Join(ic, ",")
    87  	cfg.ClusterState = embed.ClusterStateFlagNew
    88  	return cfg
    89  }
    90  
    91  func (t *testEtcdUtilSuite) urlsToStrings(urls []url.URL) []string {
    92  	ret := make([]string, 0, len(urls))
    93  	for _, u := range urls {
    94  		ret = append(ret, u.String())
    95  	}
    96  	return ret
    97  }
    98  
    99  func (t *testEtcdUtilSuite) startEtcd(c *C, cfg *embed.Config) *embed.Etcd {
   100  	e, err := embed.StartEtcd(cfg)
   101  	c.Assert(err, IsNil)
   102  
   103  	timeout := 10 * time.Second
   104  	select {
   105  	case <-e.Server.ReadyNotify():
   106  	case <-time.After(timeout):
   107  		e.Server.Stop()
   108  		c.Fatalf("start embed etcd timeout %v", timeout)
   109  	}
   110  	return e
   111  }
   112  
   113  func (t *testEtcdUtilSuite) createEtcdClient(c *C, endpoints []string) *clientv3.Client {
   114  	cli, err := CreateClient(endpoints, nil)
   115  	c.Assert(err, IsNil)
   116  	return cli
   117  }
   118  
   119  func (t *testEtcdUtilSuite) checkMember(c *C, mid uint64, m *etcdserverpb.Member, cfg *embed.Config) {
   120  	if m.Name != "" { // no name exists after `member add`
   121  		c.Assert(m.Name, Equals, cfg.Name)
   122  	}
   123  	c.Assert(m.ID, Equals, mid)
   124  	require.ElementsMatch(t.testT, m.ClientURLs, t.urlsToStrings(cfg.AdvertiseClientUrls))
   125  	require.ElementsMatch(t.testT, m.PeerURLs, t.urlsToStrings(cfg.AdvertisePeerUrls))
   126  }
   127  
   128  func (t *testEtcdUtilSuite) TestMemberUtil(c *C) {
   129  	for i := 1; i <= 3; i++ {
   130  		t.testMemberUtilInternal(c, i)
   131  	}
   132  }
   133  
   134  func (t *testEtcdUtilSuite) testMemberUtilInternal(c *C, portCount int) {
   135  	// start a etcd
   136  	cfg1 := t.newConfig(c, "etcd1", portCount)
   137  	endpoints1 := t.urlsToStrings(cfg1.ListenClientUrls)
   138  	etcd1 := t.startEtcd(c, cfg1)
   139  	defer etcd1.Close()
   140  
   141  	// list member
   142  	cli := t.createEtcdClient(c, endpoints1)
   143  	listResp1, err := ListMembers(cli)
   144  	c.Assert(err, IsNil)
   145  	c.Assert(listResp1.Members, HasLen, 1)
   146  	t.checkMember(c, uint64(etcd1.Server.ID()), listResp1.Members[0], cfg1)
   147  
   148  	// add member
   149  	cfg2 := t.newConfig(c, "etcd2", portCount)
   150  	cfg2.InitialCluster = cfg1.InitialCluster + "," + cfg2.InitialCluster
   151  	cfg2.ClusterState = embed.ClusterStateFlagExisting
   152  	addResp, err := AddMember(cli, t.urlsToStrings(cfg2.AdvertisePeerUrls))
   153  	c.Assert(err, IsNil)
   154  	c.Assert(addResp.Members, HasLen, 2)
   155  
   156  	// start the added member
   157  	etcd2 := t.startEtcd(c, cfg2)
   158  	defer etcd2.Close()
   159  	c.Assert(addResp.Member.ID, Equals, uint64(etcd2.Server.ID()))
   160  
   161  	// list member again
   162  	listResp2, err := ListMembers(cli)
   163  	c.Assert(err, IsNil)
   164  	c.Assert(listResp2.Members, HasLen, 2)
   165  	for _, m := range listResp2.Members {
   166  		switch m.ID {
   167  		case uint64(etcd1.Server.ID()):
   168  			t.checkMember(c, uint64(etcd1.Server.ID()), m, cfg1)
   169  		case uint64(etcd2.Server.ID()):
   170  			t.checkMember(c, uint64(etcd2.Server.ID()), m, cfg2)
   171  		default:
   172  			c.Fatalf("unknown member %v", m)
   173  		}
   174  	}
   175  }
   176  
   177  func (t *testEtcdUtilSuite) TestRemoveMember(c *C) {
   178  	// test remove one member that is not the one we connected to.
   179  	// if we remove the one we connected to, the test might fail, see more in https://github.com/etcd-io/etcd/pull/7242
   180  	cluster := integration.NewClusterV3(t.testT, &integration.ClusterConfig{Size: 3})
   181  	defer cluster.Terminate(t.testT)
   182  	leaderIdx := cluster.WaitLeader(t.testT)
   183  	c.Assert(leaderIdx, Not(Equals), -1)
   184  	cli := cluster.Client(leaderIdx)
   185  	respList, err := ListMembers(cli)
   186  	c.Assert(err, IsNil)
   187  	c.Assert(respList.Members, HasLen, 3)
   188  	for _, m := range respList.Members {
   189  		if m.ID != respList.Header.MemberId {
   190  			respRemove, removeErr := RemoveMember(cli, m.ID)
   191  			c.Assert(removeErr, IsNil)
   192  			c.Assert(respRemove.Members, HasLen, 2)
   193  			break
   194  		}
   195  	}
   196  	respList, err = ListMembers(cli)
   197  	c.Assert(err, IsNil)
   198  	c.Assert(respList.Members, HasLen, 2)
   199  }
   200  
   201  func (t *testEtcdUtilSuite) TestDoOpsInOneTxnWithRetry(c *C) {
   202  	var (
   203  		key1 = "/test/etcdutil/do-ops-in-one-txn-with-retry-1"
   204  		key2 = "/test/etcdutil/do-ops-in-one-txn-with-retry-2"
   205  		val1 = "foo"
   206  		val2 = "bar"
   207  		val  = "foo-bar"
   208  	)
   209  	cluster := integration.NewClusterV3(t.testT, &integration.ClusterConfig{Size: 1})
   210  	defer cluster.Terminate(t.testT)
   211  
   212  	cli := cluster.RandClient()
   213  
   214  	resp, rev1, err := DoTxnWithRepeatable(cli, ThenOpFunc(clientv3.OpPut(key1, val1), clientv3.OpPut(key2, val2)))
   215  	c.Assert(err, IsNil)
   216  	c.Assert(rev1, Greater, int64(0))
   217  	c.Assert(resp.Responses, HasLen, 2)
   218  
   219  	// both cmps are true
   220  	cmp1 := clientv3.Compare(clientv3.Value(key1), "=", val1)
   221  	cmp2 := clientv3.Compare(clientv3.Value(key2), "=", val2)
   222  	resp, rev2, err := DoTxnWithRepeatable(cli, FullOpFunc([]clientv3.Cmp{cmp1, cmp2}, []clientv3.Op{
   223  		clientv3.OpPut(key1, val), clientv3.OpPut(key2, val),
   224  	}, []clientv3.Op{}))
   225  	c.Assert(err, IsNil)
   226  	c.Assert(rev2, Greater, rev1)
   227  	c.Assert(resp.Responses, HasLen, 2)
   228  
   229  	// one of cmps are false
   230  	cmp1 = clientv3.Compare(clientv3.Value(key1), "=", val)
   231  	cmp2 = clientv3.Compare(clientv3.Value(key2), "=", val2)
   232  	resp, rev3, err := DoTxnWithRepeatable(cli, FullOpFunc([]clientv3.Cmp{cmp1, cmp2}, []clientv3.Op{}, []clientv3.Op{
   233  		clientv3.OpDelete(key1), clientv3.OpDelete(key2),
   234  	}))
   235  	c.Assert(err, IsNil)
   236  	c.Assert(rev3, Greater, rev2)
   237  	c.Assert(resp.Responses, HasLen, 2)
   238  
   239  	// enable failpoint
   240  	c.Assert(failpoint.Enable("github.com/pingcap/tiflow/dm/pkg/etcdutil/ErrNoSpace", `3*return()`), IsNil)
   241  	//nolint:errcheck
   242  	defer failpoint.Disable("github.com/pingcap/tiflow/dm/pkg/etcdutil/ErrNoSpace")
   243  
   244  	// put again
   245  	resp, rev2, err = DoTxnWithRepeatable(cli, FullOpFunc([]clientv3.Cmp{clientv3util.KeyMissing(key1), clientv3util.KeyMissing(key2)}, []clientv3.Op{
   246  		clientv3.OpPut(key1, val), clientv3.OpPut(key2, val),
   247  	}, []clientv3.Op{}))
   248  	c.Assert(err, IsNil)
   249  	c.Assert(rev2, Greater, rev1)
   250  	c.Assert(resp.Responses, HasLen, 2)
   251  }
   252  
   253  func (t *testEtcdUtilSuite) TestIsRetryableError(c *C) {
   254  	c.Assert(IsRetryableError(v3rpc.ErrCompacted), IsTrue)
   255  	c.Assert(IsRetryableError(v3rpc.ErrNoLeader), IsTrue)
   256  	c.Assert(IsRetryableError(v3rpc.ErrNoSpace), IsTrue)
   257  
   258  	c.Assert(IsRetryableError(v3rpc.ErrCorrupt), IsFalse)
   259  	c.Assert(IsRetryableError(terror.ErrDecodeEtcdKeyFail), IsFalse)
   260  	c.Assert(IsRetryableError(nil), IsFalse)
   261  }