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  }