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 }