github.com/cilium/cilium@v1.16.2/pkg/kvstore/etcd_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package kvstore 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "path" 11 "sync" 12 "testing" 13 "time" 14 15 "github.com/stretchr/testify/require" 16 etcdAPI "go.etcd.io/etcd/client/v3" 17 "golang.org/x/exp/maps" 18 "k8s.io/apimachinery/pkg/util/rand" 19 20 "github.com/cilium/cilium/pkg/testutils" 21 ) 22 23 func TestHint(t *testing.T) { 24 var err error 25 26 require.NoError(t, Hint(err)) 27 28 err = errors.New("foo bar") 29 require.ErrorContains(t, Hint(err), "foo bar") 30 31 err = fmt.Errorf("ayy lmao") 32 require.ErrorContains(t, Hint(err), "ayy lmao") 33 34 err = context.DeadlineExceeded 35 require.ErrorContains(t, Hint(err), "etcd client timeout exceeded") 36 37 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) 38 defer cancel() 39 40 <-ctx.Done() 41 err = ctx.Err() 42 43 require.ErrorContains(t, Hint(err), "etcd client timeout exceeded") 44 } 45 46 func setupEtcdLockedSuite(tb testing.TB) *etcdAPI.Client { 47 testutils.IntegrationTest(tb) 48 49 SetupDummyWithConfigOpts(tb, "etcd", opts("etcd")) 50 51 // setup client 52 cfg := etcdAPI.Config{} 53 cfg.Endpoints = []string{etcdDummyAddress} 54 cfg.DialTimeout = 0 55 cli, err := etcdAPI.New(cfg) 56 cfg.DialTimeout = 0 57 require.NoError(tb, err) 58 tb.Cleanup(func() { require.NoError(tb, cli.Close()) }) 59 60 return cli 61 } 62 63 func TestGetIfLocked(t *testing.T) { 64 cl := setupEtcdLockedSuite(t) 65 66 randomPath := t.TempDir() 67 type args struct { 68 key string 69 lock KVLocker 70 } 71 type wanted struct { 72 err error 73 value []byte 74 } 75 tests := []struct { 76 name string 77 setupArgs func() args 78 setupWanted func() wanted 79 cleanup func(args args) error 80 }{ 81 { 82 name: "getting locked path", 83 setupArgs: func() args { 84 key := randomPath + "foo" 85 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 86 require.NoError(t, err) 87 _, err = cl.Put(context.Background(), key, "bar") 88 require.NoError(t, err) 89 90 return args{ 91 key: key, 92 lock: kvlocker, 93 } 94 }, 95 setupWanted: func() wanted { 96 return wanted{ 97 err: nil, 98 value: []byte("bar"), 99 } 100 }, 101 cleanup: func(args args) error { 102 _, err := cl.Delete(context.Background(), args.key) 103 if err != nil { 104 return err 105 } 106 return args.lock.Unlock(context.TODO()) 107 }, 108 }, 109 { 110 name: "getting locked path with no value", 111 setupArgs: func() args { 112 key := randomPath + "foo" 113 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 114 require.NoError(t, err) 115 _, err = cl.Delete(context.Background(), key) 116 require.NoError(t, err) 117 118 return args{ 119 key: key, 120 lock: kvlocker, 121 } 122 }, 123 setupWanted: func() wanted { 124 return wanted{ 125 err: nil, 126 value: nil, 127 } 128 }, 129 cleanup: func(args args) error { 130 return args.lock.Unlock(context.TODO()) 131 }, 132 }, 133 { 134 name: "getting locked path where lock was lost", 135 setupArgs: func() args { 136 key := randomPath + "foo" 137 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 138 require.NoError(t, err) 139 err = kvlocker.Unlock(context.TODO()) 140 require.NoError(t, err) 141 142 _, err = cl.Put(context.Background(), key, "bar") 143 require.NoError(t, err) 144 145 return args{ 146 key: key, 147 lock: kvlocker, 148 } 149 }, 150 setupWanted: func() wanted { 151 return wanted{ 152 err: ErrLockLeaseExpired, 153 value: nil, 154 } 155 }, 156 cleanup: func(args args) error { 157 _, err := cl.Delete(context.Background(), args.key) 158 return err 159 }, 160 }, 161 } 162 for _, tt := range tests { 163 t.Log(tt.name) 164 args := tt.setupArgs() 165 want := tt.setupWanted() 166 value, err := Client().GetIfLocked(context.TODO(), args.key, args.lock) 167 require.Equal(t, want.err, err) 168 require.EqualValues(t, want.value, value) 169 err = tt.cleanup(args) 170 require.NoError(t, err) 171 } 172 } 173 174 func TestDeleteIfLocked(t *testing.T) { 175 e := setupEtcdLockedSuite(t) 176 177 randomPath := t.TempDir() 178 type args struct { 179 key string 180 lock KVLocker 181 } 182 type wanted struct { 183 err error 184 } 185 tests := []struct { 186 name string 187 setupArgs func() args 188 setupWanted func() wanted 189 cleanup func(args args) error 190 }{ 191 { 192 name: "deleting locked path", 193 setupArgs: func() args { 194 key := randomPath + "foo" 195 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 196 require.NoError(t, err) 197 _, err = e.Put(context.Background(), key, "bar") 198 require.NoError(t, err) 199 200 return args{ 201 key: key, 202 lock: kvlocker, 203 } 204 }, 205 setupWanted: func() wanted { 206 return wanted{ 207 err: nil, 208 } 209 }, 210 cleanup: func(args args) error { 211 key := randomPath + "foo" 212 // verify that key was actually deleted 213 gr, err := e.Get(context.Background(), key) 214 require.NoError(t, err) 215 require.Equal(t, int64(0), gr.Count) 216 217 return args.lock.Unlock(context.TODO()) 218 }, 219 }, 220 { 221 name: "deleting locked path with no value", 222 setupArgs: func() args { 223 key := randomPath + "foo" 224 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 225 require.NoError(t, err) 226 227 _, err = e.Delete(context.Background(), key) 228 require.NoError(t, err) 229 230 return args{ 231 key: key, 232 lock: kvlocker, 233 } 234 }, 235 setupWanted: func() wanted { 236 return wanted{ 237 err: nil, 238 } 239 }, 240 cleanup: func(args args) error { 241 key := randomPath + "foo" 242 // verify that key was actually deleted (this should not matter 243 // as the key was never in the kvstore but still) 244 gr, err := e.Get(context.Background(), key) 245 require.NoError(t, err) 246 require.Equal(t, int64(0), gr.Count) 247 248 return args.lock.Unlock(context.TODO()) 249 }, 250 }, 251 { 252 name: "deleting locked path where lock was lost", 253 setupArgs: func() args { 254 key := randomPath + "foo" 255 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 256 require.NoError(t, err) 257 _, err = e.Put(context.Background(), key, "bar") 258 require.NoError(t, err) 259 err = kvlocker.Unlock(context.TODO()) 260 require.NoError(t, err) 261 262 return args{ 263 key: key, 264 lock: kvlocker, 265 } 266 }, 267 setupWanted: func() wanted { 268 return wanted{ 269 err: ErrLockLeaseExpired, 270 } 271 }, 272 cleanup: func(args args) error { 273 key := randomPath + "foo" 274 // If the lock was lost it means the value still exists 275 value, err := e.Get(context.Background(), key) 276 require.NoError(t, err) 277 require.Equal(t, int64(1), value.Count) 278 require.EqualValues(t, []byte("bar"), value.Kvs[0].Value) 279 return nil 280 }, 281 }, 282 } 283 for _, tt := range tests { 284 t.Log(tt.name) 285 args := tt.setupArgs() 286 want := tt.setupWanted() 287 err := Client().DeleteIfLocked(context.TODO(), args.key, args.lock) 288 require.Equal(t, want.err, err) 289 err = tt.cleanup(args) 290 require.NoError(t, err) 291 } 292 } 293 294 func TestUpdateIfLocked(t *testing.T) { 295 e := setupEtcdLockedSuite(t) 296 297 randomPath := t.TempDir() 298 type args struct { 299 key string 300 lock KVLocker 301 newValue []byte 302 lease bool 303 } 304 type wanted struct { 305 err error 306 } 307 tests := []struct { 308 name string 309 setupArgs func() args 310 setupWanted func() wanted 311 cleanup func(args args) error 312 }{ 313 { 314 name: "update locked path without lease", 315 setupArgs: func() args { 316 key := randomPath + "foo" 317 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 318 require.NoError(t, err) 319 _, err = e.Put(context.Background(), key, "bar") 320 require.NoError(t, err) 321 322 return args{ 323 key: key, 324 lock: kvlocker, 325 newValue: []byte("newbar"), 326 } 327 }, 328 setupWanted: func() wanted { 329 return wanted{ 330 err: nil, 331 } 332 }, 333 cleanup: func(args args) error { 334 key := randomPath + "foo" 335 // verify that key was actually updated 336 gr, err := e.Get(context.Background(), key) 337 require.NoError(t, err) 338 require.Equal(t, int64(1), gr.Count) 339 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 340 341 return args.lock.Unlock(context.TODO()) 342 }, 343 }, 344 { 345 name: "update locked path with no value without lease", 346 setupArgs: func() args { 347 key := randomPath + "foo" 348 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 349 require.NoError(t, err) 350 351 _, err = e.Delete(context.Background(), key) 352 require.NoError(t, err) 353 354 return args{ 355 key: key, 356 lock: kvlocker, 357 newValue: []byte("newbar"), 358 } 359 }, 360 setupWanted: func() wanted { 361 return wanted{ 362 err: nil, 363 } 364 }, 365 cleanup: func(args args) error { 366 key := randomPath + "foo" 367 // a key that was updated with no value will create a new value 368 gr, err := e.Get(context.Background(), key) 369 require.NoError(t, err) 370 require.Equal(t, int64(1), gr.Count) 371 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 372 373 return args.lock.Unlock(context.TODO()) 374 }, 375 }, 376 { 377 name: "update locked path where lock was lost without lease", 378 setupArgs: func() args { 379 key := randomPath + "foo" 380 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 381 require.NoError(t, err) 382 _, err = e.Put(context.Background(), key, "bar") 383 require.NoError(t, err) 384 err = kvlocker.Unlock(context.TODO()) 385 require.NoError(t, err) 386 387 return args{ 388 key: key, 389 lock: kvlocker, 390 } 391 }, 392 setupWanted: func() wanted { 393 return wanted{ 394 err: ErrLockLeaseExpired, 395 } 396 }, 397 cleanup: func(args args) error { 398 key := randomPath + "foo" 399 // verify that key was actually updated 400 gr, err := e.Get(context.Background(), key) 401 require.NoError(t, err) 402 require.Equal(t, int64(1), gr.Count) 403 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 404 return nil 405 }, 406 }, 407 { 408 name: "update locked path with lease", 409 setupArgs: func() args { 410 key := randomPath + "foo" 411 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 412 require.NoError(t, err) 413 _, err = e.Put(context.Background(), key, "bar") 414 require.NoError(t, err) 415 416 return args{ 417 key: key, 418 lock: kvlocker, 419 newValue: []byte("newbar"), 420 lease: true, 421 } 422 }, 423 setupWanted: func() wanted { 424 return wanted{ 425 err: nil, 426 } 427 }, 428 cleanup: func(args args) error { 429 key := randomPath + "foo" 430 // verify that key was actually updated 431 gr, err := e.Get(context.Background(), key) 432 require.NoError(t, err) 433 require.Equal(t, int64(1), gr.Count) 434 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 435 436 return args.lock.Unlock(context.TODO()) 437 }, 438 }, 439 { 440 name: "update locked path with no value with lease", 441 setupArgs: func() args { 442 key := randomPath + "foo" 443 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 444 require.NoError(t, err) 445 446 _, err = e.Delete(context.Background(), key) 447 require.NoError(t, err) 448 449 return args{ 450 key: key, 451 lock: kvlocker, 452 newValue: []byte("newbar"), 453 lease: true, 454 } 455 }, 456 setupWanted: func() wanted { 457 return wanted{ 458 err: nil, 459 } 460 }, 461 cleanup: func(args args) error { 462 key := randomPath + "foo" 463 // a key that was updated with no value will create a new value 464 gr, err := e.Get(context.Background(), key) 465 require.NoError(t, err) 466 require.Equal(t, int64(1), gr.Count) 467 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 468 469 return args.lock.Unlock(context.TODO()) 470 }, 471 }, 472 { 473 name: "update locked path where lock was lost with lease", 474 setupArgs: func() args { 475 key := randomPath + "foo" 476 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 477 require.NoError(t, err) 478 _, err = e.Put(context.Background(), key, "bar") 479 require.NoError(t, err) 480 err = kvlocker.Unlock(context.TODO()) 481 require.NoError(t, err) 482 483 return args{ 484 key: key, 485 lock: kvlocker, 486 lease: true, 487 } 488 }, 489 setupWanted: func() wanted { 490 return wanted{ 491 err: ErrLockLeaseExpired, 492 } 493 }, 494 cleanup: func(args args) error { 495 key := randomPath + "foo" 496 // verify that key was actually updated 497 gr, err := e.Get(context.Background(), key) 498 require.NoError(t, err) 499 require.Equal(t, int64(1), gr.Count) 500 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 501 return nil 502 }, 503 }, 504 } 505 for _, tt := range tests { 506 t.Log(tt.name) 507 args := tt.setupArgs() 508 want := tt.setupWanted() 509 err := Client().UpdateIfLocked(context.Background(), args.key, args.newValue, args.lease, args.lock) 510 require.Equal(t, want.err, err) 511 err = tt.cleanup(args) 512 require.NoError(t, err) 513 } 514 } 515 516 func TestUpdateIfDifferentIfLocked(t *testing.T) { 517 e := setupEtcdLockedSuite(t) 518 519 randomPath := t.TempDir() 520 type args struct { 521 key string 522 lock KVLocker 523 newValue []byte 524 lease bool 525 } 526 type wanted struct { 527 err error 528 updated bool 529 } 530 tests := []struct { 531 name string 532 setupArgs func() args 533 setupWanted func() wanted 534 cleanup func(args args) error 535 }{ 536 { 537 name: "update locked path without lease", 538 setupArgs: func() args { 539 key := randomPath + "foo" 540 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 541 require.NoError(t, err) 542 _, err = e.Put(context.Background(), key, "bar") 543 require.NoError(t, err) 544 545 return args{ 546 key: key, 547 lock: kvlocker, 548 newValue: []byte("newbar"), 549 } 550 }, 551 setupWanted: func() wanted { 552 return wanted{ 553 err: nil, 554 updated: true, 555 } 556 }, 557 cleanup: func(args args) error { 558 key := randomPath + "foo" 559 // verify that key was actually updated 560 gr, err := e.Get(context.Background(), key) 561 require.NoError(t, err) 562 require.Equal(t, int64(1), gr.Count) 563 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 564 _, err = e.Delete(context.Background(), key) 565 require.NoError(t, err) 566 return args.lock.Unlock(context.TODO()) 567 }, 568 }, 569 { 570 name: "update locked path without lease and with same value", 571 setupArgs: func() args { 572 key := randomPath + "foo" 573 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 574 require.NoError(t, err) 575 _, err = e.Put(context.Background(), key, "bar") 576 require.NoError(t, err) 577 578 return args{ 579 key: key, 580 lock: kvlocker, 581 newValue: []byte("bar"), 582 } 583 }, 584 setupWanted: func() wanted { 585 return wanted{ 586 err: nil, 587 } 588 }, 589 cleanup: func(args args) error { 590 key := randomPath + "foo" 591 // verify that key was actually updated 592 gr, err := e.Get(context.Background(), key) 593 require.NoError(t, err) 594 require.Equal(t, int64(1), gr.Count) 595 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 596 597 return args.lock.Unlock(context.TODO()) 598 }, 599 }, 600 { 601 name: "update locked path with no value without lease", 602 setupArgs: func() args { 603 key := randomPath + "foo" 604 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 605 require.NoError(t, err) 606 607 _, err = e.Delete(context.Background(), key) 608 require.NoError(t, err) 609 610 return args{ 611 key: key, 612 lock: kvlocker, 613 newValue: []byte("newbar"), 614 } 615 }, 616 setupWanted: func() wanted { 617 return wanted{ 618 err: nil, 619 updated: true, 620 } 621 }, 622 cleanup: func(args args) error { 623 key := randomPath + "foo" 624 // a key that was updated with no value will create a new value 625 gr, err := e.Get(context.Background(), key) 626 require.NoError(t, err) 627 require.Equal(t, int64(1), gr.Count) 628 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 629 _, err = e.Delete(context.Background(), key) 630 require.NoError(t, err) 631 return args.lock.Unlock(context.TODO()) 632 }, 633 }, 634 { 635 name: "update locked path where lock was lost without lease", 636 setupArgs: func() args { 637 key := randomPath + "foo" 638 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 639 require.NoError(t, err) 640 _, err = e.Put(context.Background(), key, "bar") 641 require.NoError(t, err) 642 err = kvlocker.Unlock(context.TODO()) 643 require.NoError(t, err) 644 645 return args{ 646 key: key, 647 newValue: []byte("baz"), 648 lock: kvlocker, 649 } 650 }, 651 setupWanted: func() wanted { 652 return wanted{ 653 err: ErrLockLeaseExpired, 654 } 655 }, 656 cleanup: func(args args) error { 657 key := randomPath + "foo" 658 // verify that key was actually updated 659 gr, err := e.Get(context.Background(), key) 660 require.NoError(t, err) 661 require.Equal(t, int64(1), gr.Count) 662 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 663 _, err = e.Delete(context.Background(), key) 664 require.NoError(t, err) 665 return nil 666 }, 667 }, 668 { 669 name: "update locked path with lease", 670 setupArgs: func() args { 671 key := randomPath + "foo" 672 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 673 require.NoError(t, err) 674 _, err = e.Put(context.Background(), key, "bar") 675 require.NoError(t, err) 676 677 return args{ 678 key: key, 679 lock: kvlocker, 680 newValue: []byte("newbar"), 681 lease: true, 682 } 683 }, 684 setupWanted: func() wanted { 685 return wanted{ 686 err: nil, 687 updated: true, 688 } 689 }, 690 cleanup: func(args args) error { 691 key := randomPath + "foo" 692 // verify that key was actually updated 693 gr, err := e.Get(context.Background(), key) 694 require.NoError(t, err) 695 require.Equal(t, int64(1), gr.Count) 696 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 697 _, err = e.Delete(context.Background(), key) 698 require.NoError(t, err) 699 return args.lock.Unlock(context.TODO()) 700 }, 701 }, 702 { 703 name: "update locked path with no value with lease", 704 setupArgs: func() args { 705 key := randomPath + "foo" 706 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 707 require.NoError(t, err) 708 709 _, err = e.Delete(context.Background(), key) 710 require.NoError(t, err) 711 712 return args{ 713 key: key, 714 lock: kvlocker, 715 newValue: []byte("newbar"), 716 lease: true, 717 } 718 }, 719 setupWanted: func() wanted { 720 return wanted{ 721 err: nil, 722 updated: true, 723 } 724 }, 725 cleanup: func(args args) error { 726 key := randomPath + "foo" 727 // a key that was updated with no value will create a new value 728 gr, err := e.Get(context.Background(), key) 729 require.NoError(t, err) 730 require.Equal(t, int64(1), gr.Count) 731 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 732 733 _, err = e.Delete(context.Background(), key) 734 require.NoError(t, err) 735 736 return args.lock.Unlock(context.TODO()) 737 }, 738 }, 739 { 740 name: "update locked path with lease and with same value", 741 setupArgs: func() args { 742 key := randomPath + "foo" 743 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 744 require.NoError(t, err) 745 created, err := Client().CreateOnly(context.Background(), key, []byte("bar"), true) 746 require.NoError(t, err) 747 require.Equal(t, true, created) 748 749 return args{ 750 key: key, 751 lock: kvlocker, 752 newValue: []byte("bar"), 753 lease: true, 754 } 755 }, 756 setupWanted: func() wanted { 757 return wanted{ 758 err: nil, 759 } 760 }, 761 cleanup: func(args args) error { 762 key := randomPath + "foo" 763 // verify that key was actually updated 764 gr, err := e.Get(context.Background(), key) 765 require.NoError(t, err) 766 require.Equal(t, int64(1), gr.Count) 767 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 768 _, err = e.Delete(context.Background(), key) 769 require.NoError(t, err) 770 return args.lock.Unlock(context.TODO()) 771 }, 772 }, 773 { 774 name: "update locked path where lock was lost with lease", 775 setupArgs: func() args { 776 key := randomPath + "foo" 777 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 778 require.NoError(t, err) 779 _, err = e.Put(context.Background(), key, "bar") 780 require.NoError(t, err) 781 err = kvlocker.Unlock(context.TODO()) 782 require.NoError(t, err) 783 784 return args{ 785 key: key, 786 lock: kvlocker, 787 lease: true, 788 } 789 }, 790 setupWanted: func() wanted { 791 return wanted{ 792 err: ErrLockLeaseExpired, 793 } 794 }, 795 cleanup: func(args args) error { 796 key := randomPath + "foo" 797 // verify that key was actually updated 798 gr, err := e.Get(context.Background(), key) 799 require.NoError(t, err) 800 require.Equal(t, int64(1), gr.Count) 801 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 802 return nil 803 }, 804 }, 805 } 806 for _, tt := range tests { 807 t.Log(tt.name) 808 args := tt.setupArgs() 809 want := tt.setupWanted() 810 updated, err := Client().UpdateIfDifferentIfLocked(context.Background(), args.key, args.newValue, args.lease, args.lock) 811 require.Equal(t, want.err, err) 812 require.Equal(t, want.updated, updated) 813 err = tt.cleanup(args) 814 require.NoError(t, err) 815 } 816 } 817 818 func TestCreateOnlyIfLocked(t *testing.T) { 819 e := setupEtcdLockedSuite(t) 820 821 randomPath := t.TempDir() 822 type args struct { 823 key string 824 lock KVLocker 825 newValue []byte 826 lease bool 827 } 828 type wanted struct { 829 err error 830 created bool 831 } 832 tests := []struct { 833 name string 834 setupArgs func() args 835 setupWanted func() wanted 836 cleanup func(args args) error 837 }{ 838 { 839 name: "create only locked path without lease", 840 setupArgs: func() args { 841 key := randomPath + "foo" 842 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 843 require.NoError(t, err) 844 845 _, err = e.Delete(context.Background(), key) 846 require.NoError(t, err) 847 848 return args{ 849 key: key, 850 lock: kvlocker, 851 newValue: []byte("newbar"), 852 } 853 }, 854 setupWanted: func() wanted { 855 return wanted{ 856 err: nil, 857 created: true, 858 } 859 }, 860 cleanup: func(args args) error { 861 key := randomPath + "foo" 862 // verify that key was actually created 863 gr, err := e.Get(context.Background(), key) 864 require.NoError(t, err) 865 require.Equal(t, int64(1), gr.Count) 866 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 867 868 return args.lock.Unlock(context.TODO()) 869 }, 870 }, 871 { 872 name: "create only locked path with an existing value without lease", 873 setupArgs: func() args { 874 key := randomPath + "foo" 875 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 876 require.NoError(t, err) 877 878 _, err = e.Put(context.Background(), key, "bar") 879 require.NoError(t, err) 880 881 return args{ 882 key: key, 883 lock: kvlocker, 884 newValue: []byte("newbar"), 885 } 886 }, 887 setupWanted: func() wanted { 888 return wanted{ 889 err: nil, 890 } 891 }, 892 cleanup: func(args args) error { 893 key := randomPath + "foo" 894 // the key should not have been created and therefore the old 895 // value is still there 896 gr, err := e.Get(context.Background(), key) 897 require.NoError(t, err) 898 require.Equal(t, int64(1), gr.Count) 899 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 900 901 return args.lock.Unlock(context.TODO()) 902 }, 903 }, 904 { 905 name: "create only locked path where lock was lost without lease", 906 setupArgs: func() args { 907 key := randomPath + "foo" 908 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 909 require.NoError(t, err) 910 _, err = e.Delete(context.Background(), key) 911 require.NoError(t, err) 912 err = kvlocker.Unlock(context.TODO()) 913 require.NoError(t, err) 914 915 return args{ 916 key: key, 917 lock: kvlocker, 918 newValue: []byte("bar"), 919 } 920 }, 921 setupWanted: func() wanted { 922 return wanted{ 923 err: ErrLockLeaseExpired, 924 } 925 }, 926 cleanup: func(args args) error { 927 key := randomPath + "foo" 928 // verify that key was not created 929 gr, err := e.Get(context.Background(), key) 930 require.NoError(t, err) 931 require.Equal(t, int64(0), gr.Count) 932 return nil 933 }, 934 }, 935 { 936 name: "create only locked path with lease", 937 setupArgs: func() args { 938 key := randomPath + "foo" 939 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 940 require.NoError(t, err) 941 942 _, err = e.Delete(context.Background(), key) 943 require.NoError(t, err) 944 945 return args{ 946 key: key, 947 lock: kvlocker, 948 newValue: []byte("newbar"), 949 lease: true, 950 } 951 }, 952 setupWanted: func() wanted { 953 return wanted{ 954 err: nil, 955 created: true, 956 } 957 }, 958 cleanup: func(args args) error { 959 key := randomPath + "foo" 960 // verify that key was actually created 961 gr, err := e.Get(context.Background(), key) 962 require.NoError(t, err) 963 require.Equal(t, int64(1), gr.Count) 964 require.EqualValues(t, []byte("newbar"), gr.Kvs[0].Value) 965 966 return args.lock.Unlock(context.TODO()) 967 }, 968 }, 969 { 970 name: "create only locked path with an existing value with lease", 971 setupArgs: func() args { 972 key := randomPath + "foo" 973 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 974 require.NoError(t, err) 975 976 _, err = e.Put(context.Background(), key, "bar") 977 require.NoError(t, err) 978 979 return args{ 980 key: key, 981 lock: kvlocker, 982 newValue: []byte("newbar"), 983 lease: true, 984 } 985 }, 986 setupWanted: func() wanted { 987 return wanted{ 988 err: nil, 989 } 990 }, 991 cleanup: func(args args) error { 992 key := randomPath + "foo" 993 // the key should not have been created and therefore the old 994 // value is still there 995 gr, err := e.Get(context.Background(), key) 996 require.NoError(t, err) 997 require.Equal(t, int64(1), gr.Count) 998 require.EqualValues(t, []byte("bar"), gr.Kvs[0].Value) 999 1000 return args.lock.Unlock(context.TODO()) 1001 }, 1002 }, 1003 { 1004 name: "create only locked path where lock was lost with lease", 1005 setupArgs: func() args { 1006 key := randomPath + "foo" 1007 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 1008 require.NoError(t, err) 1009 _, err = e.Delete(context.Background(), key) 1010 require.NoError(t, err) 1011 err = kvlocker.Unlock(context.TODO()) 1012 require.NoError(t, err) 1013 1014 return args{ 1015 key: key, 1016 lock: kvlocker, 1017 newValue: []byte("bar"), 1018 lease: true, 1019 } 1020 }, 1021 setupWanted: func() wanted { 1022 return wanted{ 1023 err: ErrLockLeaseExpired, 1024 } 1025 }, 1026 cleanup: func(args args) error { 1027 key := randomPath + "foo" 1028 // verify that key was not created 1029 gr, err := e.Get(context.Background(), key) 1030 require.NoError(t, err) 1031 require.Equal(t, int64(0), gr.Count) 1032 return nil 1033 }, 1034 }, 1035 } 1036 for _, tt := range tests { 1037 t.Log(tt.name) 1038 args := tt.setupArgs() 1039 want := tt.setupWanted() 1040 created, err := Client().CreateOnlyIfLocked(context.Background(), args.key, args.newValue, args.lease, args.lock) 1041 require.Equal(t, want.err, err) 1042 require.Equal(t, want.created, created) 1043 err = tt.cleanup(args) 1044 require.NoError(t, err) 1045 } 1046 } 1047 1048 func TestListPrefixIfLocked(t *testing.T) { 1049 e := setupEtcdLockedSuite(t) 1050 1051 randomPath := t.TempDir() 1052 type args struct { 1053 key string 1054 lock KVLocker 1055 } 1056 type wanted struct { 1057 err error 1058 kvPairs KeyValuePairs 1059 } 1060 tests := []struct { 1061 name string 1062 setupArgs func() args 1063 setupWanted func() wanted 1064 cleanup func(args args) error 1065 }{ 1066 { 1067 name: "list prefix locked", 1068 setupArgs: func() args { 1069 key := randomPath + "foo" 1070 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 1071 require.NoError(t, err) 1072 _, err = e.Put(context.Background(), key, "bar") 1073 require.NoError(t, err) 1074 _, err = e.Put(context.Background(), key+"1", "bar1") 1075 require.NoError(t, err) 1076 1077 return args{ 1078 key: key, 1079 lock: kvlocker, 1080 } 1081 }, 1082 setupWanted: func() wanted { 1083 key := randomPath + "foo" 1084 return wanted{ 1085 err: nil, 1086 kvPairs: KeyValuePairs{ 1087 key: Value{ 1088 Data: []byte("bar"), 1089 }, 1090 key + "1": Value{ 1091 Data: []byte("bar1"), 1092 }, 1093 }, 1094 } 1095 }, 1096 cleanup: func(args args) error { 1097 _, err := e.Delete(context.Background(), args.key, etcdAPI.WithPrefix()) 1098 if err != nil { 1099 return err 1100 } 1101 return args.lock.Unlock(context.TODO()) 1102 }, 1103 }, 1104 { 1105 name: "list prefix locked with no values", 1106 setupArgs: func() args { 1107 key := randomPath + "foo" 1108 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 1109 require.NoError(t, err) 1110 _, err = e.Delete(context.Background(), key, etcdAPI.WithPrefix()) 1111 require.NoError(t, err) 1112 1113 return args{ 1114 key: key, 1115 lock: kvlocker, 1116 } 1117 }, 1118 setupWanted: func() wanted { 1119 return wanted{ 1120 err: nil, 1121 } 1122 }, 1123 cleanup: func(args args) error { 1124 return args.lock.Unlock(context.TODO()) 1125 }, 1126 }, 1127 { 1128 name: "list prefix locked where lock was lost", 1129 setupArgs: func() args { 1130 key := randomPath + "foo" 1131 kvlocker, err := Client().LockPath(context.Background(), "locks/"+key+"/.lock") 1132 require.NoError(t, err) 1133 _, err = e.Put(context.Background(), key, "bar") 1134 require.NoError(t, err) 1135 _, err = e.Put(context.Background(), key+"1", "bar1") 1136 require.NoError(t, err) 1137 err = kvlocker.Unlock(context.TODO()) 1138 require.NoError(t, err) 1139 1140 return args{ 1141 key: key, 1142 lock: kvlocker, 1143 } 1144 }, 1145 setupWanted: func() wanted { 1146 return wanted{ 1147 err: ErrLockLeaseExpired, 1148 } 1149 }, 1150 cleanup: func(args args) error { 1151 _, err := e.Delete(context.Background(), args.key) 1152 return err 1153 }, 1154 }, 1155 } 1156 for _, tt := range tests { 1157 t.Log(tt.name) 1158 args := tt.setupArgs() 1159 want := tt.setupWanted() 1160 kvPairs, err := Client().ListPrefixIfLocked(context.TODO(), args.key, args.lock) 1161 require.Equal(t, want.err, err) 1162 for k, v := range kvPairs { 1163 // We don't compare revision of the value because we can't predict 1164 // its value. 1165 v1, ok := want.kvPairs[k] 1166 require.Equal(t, true, ok) 1167 require.EqualValues(t, v1.Data, v.Data) 1168 } 1169 err = tt.cleanup(args) 1170 require.NoError(t, err) 1171 } 1172 } 1173 1174 func TestShuffleEndpoints(t *testing.T) { 1175 s1 := []string{"1", "2", "3", "4", "5"} 1176 s2 := make([]string, len(s1)) 1177 copy(s2, s1) 1178 1179 var same int 1180 for retry := 0; retry < 10; retry++ { 1181 same = 0 1182 shuffleEndpoints(s2) 1183 for i := range s1 { 1184 if s1[i] == s2[i] { 1185 same++ 1186 } 1187 } 1188 if same != len(s1) { 1189 break 1190 } 1191 } 1192 if same == len(s1) { 1193 t.Errorf("Shuffle() did not modify s2 in 10 retries") 1194 } 1195 } 1196 1197 func TestEtcdRateLimiter(t *testing.T) { 1198 testutils.IntegrationTest(t) 1199 1200 t.Run("with QPS=100", func(t *testing.T) { 1201 testEtcdRateLimiter(t, 100, 10, require.Less) 1202 }) 1203 1204 t.Run("with QPS=4", func(t *testing.T) { 1205 testEtcdRateLimiter(t, 4, 10, require.Greater) 1206 }) 1207 } 1208 1209 func testEtcdRateLimiter(t *testing.T, qps, count int, cmp func(require.TestingT, interface{}, interface{}, ...interface{})) { 1210 const ( 1211 prefix = "foo" 1212 condKey = prefix + "-cond-key" 1213 value = "bar" 1214 1215 threshold = time.Second 1216 ) 1217 1218 ctx := context.Background() 1219 getKey := func(id int) string { 1220 return fmt.Sprintf("%s-%d", prefix, id) 1221 } 1222 1223 // Initialize a separate etcd client which is not subject to any rate limiting 1224 cfg := etcdAPI.Config{ 1225 Endpoints: []string{etcdDummyAddress}, 1226 DialTimeout: 5 * time.Second, 1227 } 1228 client, err := etcdAPI.New(cfg) 1229 require.NoError(t, err) 1230 1231 t.Cleanup(func() { 1232 require.NoError(t, client.Close()) 1233 }) 1234 1235 tests := []struct { 1236 fn func(*testing.T, string, int, KVLocker) 1237 name string 1238 useKVLocker bool 1239 needCondKey bool 1240 populateKVPairs bool 1241 }{ 1242 { 1243 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1244 val, err := Client().GetIfLocked(ctx, getKey(k), locker) 1245 require.NoError(t, err) 1246 require.Equal(t, []byte(value), val) 1247 }, 1248 name: "GetIfLocked", 1249 useKVLocker: true, 1250 populateKVPairs: true, 1251 }, 1252 { 1253 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1254 val, err := Client().Get(ctx, getKey(k)) 1255 require.NoError(t, err) 1256 require.Equal(t, []byte(value), val) 1257 }, 1258 name: "Get", 1259 populateKVPairs: true, 1260 }, 1261 { 1262 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1263 kvPairs, err := Client().ListPrefix(ctx, getKey(k)) 1264 require.NoError(t, err) 1265 require.Len(t, kvPairs, 1) 1266 val, ok := kvPairs[getKey(k)] 1267 require.True(t, ok) 1268 require.Equal(t, []byte(value), val.Data) 1269 }, 1270 name: "ListPrefix", 1271 populateKVPairs: true, 1272 }, 1273 { 1274 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1275 kvPairs, err := Client().ListPrefixIfLocked(ctx, getKey(k), locker) 1276 require.NoError(t, err) 1277 require.Len(t, kvPairs, 1) 1278 val, ok := kvPairs[getKey(k)] 1279 require.True(t, ok) 1280 require.Equal(t, []byte(value), val.Data) 1281 }, 1282 name: "ListPrefixIfLocked", 1283 useKVLocker: true, 1284 populateKVPairs: true, 1285 }, 1286 { 1287 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1288 updated, err := Client().UpdateIfDifferent(ctx, getKey(k), []byte("bar-new"), true) 1289 require.NoError(t, err) 1290 require.True(t, updated) 1291 }, 1292 name: "UpdateIfDifferent", 1293 populateKVPairs: true, 1294 }, 1295 { 1296 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1297 updated, err := Client().UpdateIfDifferentIfLocked(ctx, getKey(k), []byte("bar-new"), true, locker) 1298 require.NoError(t, err) 1299 require.True(t, updated) 1300 }, 1301 name: "UpdateIfDifferentIfLocked", 1302 useKVLocker: true, 1303 populateKVPairs: true, 1304 }, 1305 { 1306 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1307 err := Client().Update(ctx, getKey(k), []byte(value), true) 1308 require.NoError(t, err) 1309 }, 1310 name: "Update", 1311 }, 1312 { 1313 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1314 err := Client().UpdateIfLocked(ctx, getKey(k), []byte(value), true, locker) 1315 require.NoError(t, err) 1316 }, 1317 name: "UpdateIfLocked", 1318 useKVLocker: true, 1319 }, 1320 { 1321 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1322 created, err := Client().CreateOnly(ctx, getKey(k), []byte(value), true) 1323 require.NoError(t, err) 1324 require.True(t, created) 1325 }, 1326 name: "CreateOnly", 1327 }, 1328 { 1329 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1330 created, err := Client().CreateOnlyIfLocked(ctx, getKey(k), []byte(value), true, locker) 1331 require.NoError(t, err) 1332 require.True(t, created) 1333 }, 1334 name: "CreateOnlyIfLocked", 1335 useKVLocker: true, 1336 }, 1337 { 1338 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1339 err := Client().Delete(ctx, getKey(k)) 1340 require.NoError(t, err) 1341 }, 1342 name: "Delete", 1343 populateKVPairs: true, 1344 }, 1345 { 1346 fn: func(t *testing.T, key string, k int, locker KVLocker) { 1347 err := Client().DeleteIfLocked(ctx, getKey(k), locker) 1348 require.NoError(t, err) 1349 }, 1350 name: "DeleteIfLocked", 1351 useKVLocker: true, 1352 populateKVPairs: true, 1353 }, 1354 { 1355 fn: func(t *testing.T, key string, k int, _ KVLocker) { 1356 err := Client().DeletePrefix(ctx, getKey(k)) 1357 require.NoError(t, err) 1358 }, 1359 name: "DeletePrefix", 1360 useKVLocker: true, 1361 populateKVPairs: true, 1362 }, 1363 } 1364 1365 for _, tt := range tests { 1366 t.Run(tt.name, func(t *testing.T) { 1367 var ( 1368 kvlocker KVLocker 1369 err error 1370 ) 1371 1372 SetupDummyWithConfigOpts(t, "etcd", map[string]string{ 1373 EtcdRateLimitOption: fmt.Sprintf("%d", qps), 1374 }) 1375 1376 if tt.populateKVPairs { 1377 for i := 0; i < count; i++ { 1378 _, err := client.Put(ctx, getKey(i), value) 1379 require.NoError(t, err) 1380 } 1381 } 1382 1383 if tt.needCondKey { 1384 _, err = client.Put(ctx, condKey, value) 1385 require.NoError(t, err) 1386 } 1387 1388 if tt.useKVLocker { 1389 kvlocker, err = Client().LockPath(ctx, "locks/"+prefix+"/.lock") 1390 require.NoError(t, err) 1391 1392 t.Cleanup(func() { 1393 require.NoError(t, kvlocker.Unlock(ctx)) 1394 }) 1395 } 1396 1397 start := time.Now() 1398 wg := sync.WaitGroup{} 1399 for i := 0; i < count; i++ { 1400 wg.Add(1) 1401 go func(wg *sync.WaitGroup, i int) { 1402 defer wg.Done() 1403 tt.fn(t, prefix, i, kvlocker) 1404 }(&wg, i) 1405 } 1406 wg.Wait() 1407 1408 cmp(t, time.Since(start), threshold) 1409 }) 1410 } 1411 } 1412 1413 type kvWrapper struct { 1414 etcdAPI.KV 1415 postGet func(context.Context) error 1416 } 1417 1418 func (kvw *kvWrapper) Get(ctx context.Context, key string, opts ...etcdAPI.OpOption) (*etcdAPI.GetResponse, error) { 1419 res, err := kvw.KV.Get(ctx, key, opts...) 1420 if err != nil { 1421 return res, err 1422 } 1423 1424 return res, kvw.postGet(ctx) 1425 } 1426 1427 func TestPaginatedList(t *testing.T) { 1428 testutils.IntegrationTest(t) 1429 SetupDummyWithConfigOpts(t, "etcd", opts("etcd")) 1430 1431 const prefix = "list/paginated" 1432 ctx := context.Background() 1433 1434 run := func(t *testing.T, batch int, withParallelOps bool) { 1435 cl := Client().(*etcdClient) 1436 keys := map[string]struct{}{ 1437 path.Join(prefix, "immortal-finch"): {}, 1438 path.Join(prefix, "rare-goshawk"): {}, 1439 path.Join(prefix, "cunning-bison"): {}, 1440 path.Join(prefix, "amusing-tick"): {}, 1441 path.Join(prefix, "prepared-shark"): {}, 1442 path.Join(prefix, "exciting-mustang"): {}, 1443 path.Join(prefix, "ethical-ibex"): {}, 1444 path.Join(prefix, "accepted-kite"): {}, 1445 path.Join(prefix, "model-javelin"): {}, 1446 path.Join(prefix, "inviting-hog"): {}, 1447 } 1448 1449 defer func(previous int) { 1450 cl.listBatchSize = previous 1451 require.Nil(t, cl.DeletePrefix(ctx, prefix)) 1452 }(cl.listBatchSize) 1453 cl.listBatchSize = batch 1454 1455 var next int64 1456 if withParallelOps { 1457 pkv := cl.client.KV 1458 defer func() { cl.client.KV = pkv }() 1459 1460 cl.client.KV = &kvWrapper{ 1461 KV: pkv, 1462 // paginatedList should observe neither upsertions nor deletions 1463 // performed after that the initial chunk of entries was retrieved. 1464 postGet: func(ctx context.Context) error { 1465 key := path.Join(prefix, rand.String(10)) 1466 res, err := cl.client.Put(ctx, key, "value") 1467 if err != nil { 1468 return err 1469 } 1470 1471 if next == 0 { 1472 next = res.Header.Revision 1473 } 1474 1475 _, err = cl.client.Delete(ctx, maps.Keys(keys)[0]) 1476 return err 1477 }, 1478 } 1479 } 1480 1481 var expected int64 1482 for key := range keys { 1483 res, err := cl.client.Put(ctx, key, "value") 1484 expected = res.Header.Revision 1485 require.NoError(t, err) 1486 } 1487 1488 kvs, found, err := cl.paginatedList(ctx, log, prefix) 1489 require.NoError(t, err) 1490 1491 for _, kv := range kvs { 1492 key := string(kv.Key) 1493 if _, ok := keys[key]; !ok { 1494 t.Fatalf("Retrieved unexpected key, key: %s", key) 1495 } 1496 delete(keys, key) 1497 } 1498 1499 require.Len(t, keys, 0) 1500 1501 // There is no guarantee that found == expected, because new operations might have occurred in parallel. 1502 if found < expected { 1503 t.Fatalf("Next revision (%d) is lower than the one of the last update (%d)", found, expected) 1504 } 1505 1506 if withParallelOps && found >= next { 1507 t.Fatalf("Next revision (%d) is higher than the one of subsequent updates (%d)", found, next) 1508 } 1509 } 1510 1511 for _, batchSize := range []int{1, 4, 11} { 1512 for _, parallelOps := range []bool{false, true} { 1513 t.Run(fmt.Sprintf("batch-size-%d-parallel-ops-%t", batchSize, parallelOps), 1514 func(t *testing.T) { run(t, batchSize, parallelOps) }) 1515 } 1516 } 1517 }