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 }