github.com/thanos-io/thanos@v0.32.5/pkg/cacheutil/memcached_client_test.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package cacheutil
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/bradfitz/gomemcache/memcache"
    15  	"github.com/go-kit/log"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
    19  	"go.uber.org/atomic"
    20  
    21  	"github.com/efficientgo/core/testutil"
    22  	"github.com/thanos-io/thanos/pkg/gate"
    23  	"github.com/thanos-io/thanos/pkg/model"
    24  )
    25  
    26  func TestMemcachedClientConfig_validate(t *testing.T) {
    27  	tests := map[string]struct {
    28  		config   MemcachedClientConfig
    29  		expected error
    30  	}{
    31  		"should pass on valid config": {
    32  			config: MemcachedClientConfig{
    33  				Addresses:                 []string{"127.0.0.1:11211"},
    34  				MaxAsyncConcurrency:       1,
    35  				DNSProviderUpdateInterval: time.Second,
    36  			},
    37  			expected: nil,
    38  		},
    39  		"should fail on no addresses": {
    40  			config: MemcachedClientConfig{
    41  				Addresses:                 []string{},
    42  				MaxAsyncConcurrency:       1,
    43  				DNSProviderUpdateInterval: time.Second,
    44  			},
    45  			expected: errMemcachedConfigNoAddrs,
    46  		},
    47  		"should fail on max_async_concurrency <= 0": {
    48  			config: MemcachedClientConfig{
    49  				Addresses:                 []string{"127.0.0.1:11211"},
    50  				MaxAsyncConcurrency:       0,
    51  				DNSProviderUpdateInterval: time.Second,
    52  			},
    53  			expected: errMemcachedMaxAsyncConcurrencyNotPositive,
    54  		},
    55  		"should fail on dns_provider_update_interval <= 0": {
    56  			config: MemcachedClientConfig{
    57  				Addresses:           []string{"127.0.0.1:11211"},
    58  				MaxAsyncConcurrency: 1,
    59  			},
    60  			expected: errMemcachedDNSUpdateIntervalNotPositive,
    61  		},
    62  	}
    63  
    64  	for testName, testData := range tests {
    65  		t.Run(testName, func(t *testing.T) {
    66  			testutil.Equals(t, testData.expected, testData.config.validate())
    67  		})
    68  	}
    69  }
    70  
    71  func TestNewMemcachedClient(t *testing.T) {
    72  	// Should return error on empty YAML config.
    73  	conf := []byte{}
    74  	cache, err := NewMemcachedClient(log.NewNopLogger(), "test", conf, nil)
    75  	testutil.NotOk(t, err)
    76  	testutil.Equals(t, (*memcachedClient)(nil), cache)
    77  
    78  	// Should return error on invalid YAML config.
    79  	conf = []byte("invalid")
    80  	cache, err = NewMemcachedClient(log.NewNopLogger(), "test", conf, nil)
    81  	testutil.NotOk(t, err)
    82  	testutil.Equals(t, (*memcachedClient)(nil), cache)
    83  
    84  	// Should instance a memcached client with minimum YAML config.
    85  	conf = []byte(`
    86  addresses:
    87    - 127.0.0.1:11211
    88    - 127.0.0.2:11211
    89  `)
    90  	cache, err = NewMemcachedClient(log.NewNopLogger(), "test", conf, nil)
    91  	testutil.Ok(t, err)
    92  	defer cache.Stop()
    93  
    94  	testutil.Equals(t, []string{"127.0.0.1:11211", "127.0.0.2:11211"}, cache.config.Addresses)
    95  	testutil.Equals(t, defaultMemcachedClientConfig.Timeout, cache.config.Timeout)
    96  	testutil.Equals(t, defaultMemcachedClientConfig.MaxIdleConnections, cache.config.MaxIdleConnections)
    97  	testutil.Equals(t, defaultMemcachedClientConfig.MaxAsyncConcurrency, cache.config.MaxAsyncConcurrency)
    98  	testutil.Equals(t, defaultMemcachedClientConfig.MaxAsyncBufferSize, cache.config.MaxAsyncBufferSize)
    99  	testutil.Equals(t, defaultMemcachedClientConfig.DNSProviderUpdateInterval, cache.config.DNSProviderUpdateInterval)
   100  	testutil.Equals(t, defaultMemcachedClientConfig.MaxGetMultiConcurrency, cache.config.MaxGetMultiConcurrency)
   101  	testutil.Equals(t, defaultMemcachedClientConfig.MaxGetMultiBatchSize, cache.config.MaxGetMultiBatchSize)
   102  	testutil.Equals(t, defaultMemcachedClientConfig.MaxItemSize, cache.config.MaxItemSize)
   103  
   104  	// Should instance a memcached client with configured YAML config.
   105  	conf = []byte(`
   106  addresses:
   107    - 127.0.0.1:11211
   108    - 127.0.0.2:11211
   109  timeout: 1s
   110  max_idle_connections: 1
   111  max_async_concurrency: 1
   112  max_async_buffer_size: 1
   113  max_get_multi_concurrency: 1
   114  max_item_size: 1MiB
   115  max_get_multi_batch_size: 1
   116  dns_provider_update_interval: 1s
   117  `)
   118  	cache, err = NewMemcachedClient(log.NewNopLogger(), "test", conf, nil)
   119  	testutil.Ok(t, err)
   120  	defer cache.Stop()
   121  
   122  	testutil.Equals(t, []string{"127.0.0.1:11211", "127.0.0.2:11211"}, cache.config.Addresses)
   123  	testutil.Equals(t, 1*time.Second, cache.config.Timeout)
   124  	testutil.Equals(t, 1, cache.config.MaxIdleConnections)
   125  	testutil.Equals(t, 1, cache.config.MaxAsyncConcurrency)
   126  	testutil.Equals(t, 1, cache.config.MaxAsyncBufferSize)
   127  	testutil.Equals(t, 1*time.Second, cache.config.DNSProviderUpdateInterval)
   128  	testutil.Equals(t, 1, cache.config.MaxGetMultiConcurrency)
   129  	testutil.Equals(t, 1, cache.config.MaxGetMultiBatchSize)
   130  	testutil.Equals(t, model.Bytes(1024*1024), cache.config.MaxItemSize)
   131  }
   132  
   133  func TestMemcachedClient_SetAsync(t *testing.T) {
   134  	ctx := context.Background()
   135  	config := defaultMemcachedClientConfig
   136  	config.Addresses = []string{"127.0.0.1:11211"}
   137  	backendMock := newMemcachedClientBackendMock()
   138  
   139  	client, err := prepare(config, backendMock)
   140  	testutil.Ok(t, err)
   141  	defer client.Stop()
   142  
   143  	testutil.Ok(t, client.SetAsync("key-1", []byte("value-1"), time.Second))
   144  	testutil.Ok(t, client.SetAsync("key-2", []byte("value-2"), time.Second))
   145  	testutil.Ok(t, backendMock.waitItems(2))
   146  
   147  	actual, err := client.getMultiSingle(ctx, []string{"key-1", "key-2"})
   148  	testutil.Ok(t, err)
   149  	testutil.Equals(t, []byte("value-1"), actual["key-1"].Value)
   150  	testutil.Equals(t, []byte("value-2"), actual["key-2"].Value)
   151  
   152  	testutil.Equals(t, 2.0, prom_testutil.ToFloat64(client.operations.WithLabelValues(opSet)))
   153  	testutil.Equals(t, 1.0, prom_testutil.ToFloat64(client.operations.WithLabelValues(opGetMulti)))
   154  	testutil.Equals(t, 0.0, prom_testutil.ToFloat64(client.failures.WithLabelValues(opSet, reasonOther)))
   155  	testutil.Equals(t, 0.0, prom_testutil.ToFloat64(client.skipped.WithLabelValues(opSet, reasonMaxItemSize)))
   156  }
   157  
   158  func TestMemcachedClient_SetAsyncWithCustomMaxItemSize(t *testing.T) {
   159  	ctx := context.Background()
   160  	config := defaultMemcachedClientConfig
   161  	config.Addresses = []string{"127.0.0.1:11211"}
   162  	config.MaxItemSize = model.Bytes(10)
   163  	backendMock := newMemcachedClientBackendMock()
   164  
   165  	client, err := prepare(config, backendMock)
   166  	testutil.Ok(t, err)
   167  	defer client.Stop()
   168  
   169  	testutil.Ok(t, client.SetAsync("key-1", []byte("value-1"), time.Second))
   170  	testutil.Ok(t, client.SetAsync("key-2", []byte("value-2-too-long-to-be-stored"), time.Second))
   171  	testutil.Ok(t, backendMock.waitItems(1))
   172  
   173  	actual, err := client.getMultiSingle(ctx, []string{"key-1", "key-2"})
   174  	testutil.Ok(t, err)
   175  	testutil.Equals(t, []byte("value-1"), actual["key-1"].Value)
   176  	testutil.Equals(t, (*memcache.Item)(nil), actual["key-2"])
   177  
   178  	testutil.Equals(t, 1.0, prom_testutil.ToFloat64(client.operations.WithLabelValues(opSet)))
   179  	testutil.Equals(t, 1.0, prom_testutil.ToFloat64(client.operations.WithLabelValues(opGetMulti)))
   180  	testutil.Equals(t, 0.0, prom_testutil.ToFloat64(client.failures.WithLabelValues(opSet, reasonOther)))
   181  	testutil.Equals(t, 1.0, prom_testutil.ToFloat64(client.skipped.WithLabelValues(opSet, reasonMaxItemSize)))
   182  }
   183  
   184  func TestMemcachedClient_GetMulti(t *testing.T) {
   185  	tests := map[string]struct {
   186  		maxBatchSize           int
   187  		maxConcurrency         int
   188  		mockedGetMultiErrors   int
   189  		initialItems           []memcache.Item
   190  		getKeys                []string
   191  		expectedHits           map[string][]byte
   192  		expectedGetMultiCount  int
   193  		expectedGateStartCount int
   194  	}{
   195  		"should fetch keys in a single batch if the input keys is <= the max batch size": {
   196  			maxBatchSize:   2,
   197  			maxConcurrency: 5,
   198  			initialItems: []memcache.Item{
   199  				{Key: "key-1", Value: []byte("value-1")},
   200  				{Key: "key-2", Value: []byte("value-2")},
   201  			},
   202  			getKeys: []string{"key-1", "key-2"},
   203  			expectedHits: map[string][]byte{
   204  				"key-1": []byte("value-1"),
   205  				"key-2": []byte("value-2"),
   206  			},
   207  			expectedGetMultiCount:  1,
   208  			expectedGateStartCount: 1,
   209  		},
   210  		"should fetch keys in multiple batches if the input keys is > the max batch size": {
   211  			maxBatchSize:   2,
   212  			maxConcurrency: 5,
   213  			initialItems: []memcache.Item{
   214  				{Key: "key-1", Value: []byte("value-1")},
   215  				{Key: "key-2", Value: []byte("value-2")},
   216  				{Key: "key-3", Value: []byte("value-3")},
   217  			},
   218  			getKeys: []string{"key-1", "key-2", "key-3"},
   219  			expectedHits: map[string][]byte{
   220  				"key-1": []byte("value-1"),
   221  				"key-2": []byte("value-2"),
   222  				"key-3": []byte("value-3"),
   223  			},
   224  			expectedGetMultiCount:  2,
   225  			expectedGateStartCount: 2,
   226  		},
   227  		"should fetch keys in multiple batches on input keys exact multiple of batch size": {
   228  			maxBatchSize:   2,
   229  			maxConcurrency: 5,
   230  			initialItems: []memcache.Item{
   231  				{Key: "key-1", Value: []byte("value-1")},
   232  				{Key: "key-2", Value: []byte("value-2")},
   233  				{Key: "key-3", Value: []byte("value-3")},
   234  				{Key: "key-4", Value: []byte("value-4")},
   235  			},
   236  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   237  			expectedHits: map[string][]byte{
   238  				"key-1": []byte("value-1"),
   239  				"key-2": []byte("value-2"),
   240  				"key-3": []byte("value-3"),
   241  				"key-4": []byte("value-4"),
   242  			},
   243  			expectedGetMultiCount:  2,
   244  			expectedGateStartCount: 2,
   245  		},
   246  		"should fetch keys in multiple batches on input keys exact multiple of batch size with max concurrency disabled (0)": {
   247  			maxBatchSize:   2,
   248  			maxConcurrency: 0,
   249  			initialItems: []memcache.Item{
   250  				{Key: "key-1", Value: []byte("value-1")},
   251  				{Key: "key-2", Value: []byte("value-2")},
   252  				{Key: "key-3", Value: []byte("value-3")},
   253  				{Key: "key-4", Value: []byte("value-4")},
   254  			},
   255  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   256  			expectedHits: map[string][]byte{
   257  				"key-1": []byte("value-1"),
   258  				"key-2": []byte("value-2"),
   259  				"key-3": []byte("value-3"),
   260  				"key-4": []byte("value-4"),
   261  			},
   262  			expectedGetMultiCount:  2,
   263  			expectedGateStartCount: 0,
   264  		},
   265  		"should fetch keys in multiple batches on input keys exact multiple of batch size with max concurrency lower than the batches": {
   266  			maxBatchSize:   1,
   267  			maxConcurrency: 1,
   268  			initialItems: []memcache.Item{
   269  				{Key: "key-1", Value: []byte("value-1")},
   270  				{Key: "key-2", Value: []byte("value-2")},
   271  				{Key: "key-3", Value: []byte("value-3")},
   272  				{Key: "key-4", Value: []byte("value-4")},
   273  			},
   274  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   275  			expectedHits: map[string][]byte{
   276  				"key-1": []byte("value-1"),
   277  				"key-2": []byte("value-2"),
   278  				"key-3": []byte("value-3"),
   279  				"key-4": []byte("value-4"),
   280  			},
   281  			expectedGetMultiCount:  4,
   282  			expectedGateStartCount: 4,
   283  		},
   284  		"should fetch keys in a single batch if max batch size is disabled (0)": {
   285  			maxBatchSize:   0,
   286  			maxConcurrency: 5,
   287  			initialItems: []memcache.Item{
   288  				{Key: "key-1", Value: []byte("value-1")},
   289  				{Key: "key-2", Value: []byte("value-2")},
   290  				{Key: "key-3", Value: []byte("value-3")},
   291  				{Key: "key-4", Value: []byte("value-4")},
   292  			},
   293  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   294  			expectedHits: map[string][]byte{
   295  				"key-1": []byte("value-1"),
   296  				"key-2": []byte("value-2"),
   297  				"key-3": []byte("value-3"),
   298  				"key-4": []byte("value-4"),
   299  			},
   300  			expectedGetMultiCount:  1,
   301  			expectedGateStartCount: 1,
   302  		},
   303  		"should fetch keys in a single batch if max batch size is disabled (0) and max concurrency is disabled (0)": {
   304  			maxBatchSize:   0,
   305  			maxConcurrency: 0,
   306  			initialItems: []memcache.Item{
   307  				{Key: "key-1", Value: []byte("value-1")},
   308  				{Key: "key-2", Value: []byte("value-2")},
   309  				{Key: "key-3", Value: []byte("value-3")},
   310  				{Key: "key-4", Value: []byte("value-4")},
   311  			},
   312  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   313  			expectedHits: map[string][]byte{
   314  				"key-1": []byte("value-1"),
   315  				"key-2": []byte("value-2"),
   316  				"key-3": []byte("value-3"),
   317  				"key-4": []byte("value-4"),
   318  			},
   319  			expectedGetMultiCount:  1,
   320  			expectedGateStartCount: 0,
   321  		},
   322  		"should return no hits on all keys missing": {
   323  			maxBatchSize:   2,
   324  			maxConcurrency: 5,
   325  			initialItems: []memcache.Item{
   326  				{Key: "key-1", Value: []byte("value-1")},
   327  				{Key: "key-2", Value: []byte("value-2")},
   328  			},
   329  			getKeys: []string{"key-1", "key-2", "key-3", "key-4"},
   330  			expectedHits: map[string][]byte{
   331  				"key-1": []byte("value-1"),
   332  				"key-2": []byte("value-2"),
   333  			},
   334  			expectedGetMultiCount:  2,
   335  			expectedGateStartCount: 2,
   336  		},
   337  		"should return no hits on partial errors while fetching batches and no items found": {
   338  			maxBatchSize:         2,
   339  			maxConcurrency:       5,
   340  			mockedGetMultiErrors: 1,
   341  			initialItems: []memcache.Item{
   342  				{Key: "key-1", Value: []byte("value-1")},
   343  				{Key: "key-2", Value: []byte("value-2")},
   344  				{Key: "key-3", Value: []byte("value-3")},
   345  			},
   346  			getKeys:                []string{"key-5", "key-6", "key-7"},
   347  			expectedHits:           map[string][]byte{},
   348  			expectedGetMultiCount:  2,
   349  			expectedGateStartCount: 2,
   350  		},
   351  		"should return no hits on all errors while fetching batches": {
   352  			maxBatchSize:         2,
   353  			maxConcurrency:       5,
   354  			mockedGetMultiErrors: 2,
   355  			initialItems: []memcache.Item{
   356  				{Key: "key-1", Value: []byte("value-1")},
   357  				{Key: "key-2", Value: []byte("value-2")},
   358  				{Key: "key-3", Value: []byte("value-3")},
   359  			},
   360  			getKeys:                []string{"key-5", "key-6", "key-7"},
   361  			expectedHits:           nil,
   362  			expectedGetMultiCount:  2,
   363  			expectedGateStartCount: 2,
   364  		},
   365  	}
   366  
   367  	for testName, testData := range tests {
   368  		t.Run(testName, func(t *testing.T) {
   369  			ctx := context.Background()
   370  			config := defaultMemcachedClientConfig
   371  			config.Addresses = []string{"127.0.0.1:11211"}
   372  			config.MaxGetMultiBatchSize = testData.maxBatchSize
   373  			config.MaxGetMultiConcurrency = testData.maxConcurrency
   374  
   375  			backendMock := newMemcachedClientBackendMock()
   376  			backendMock.getMultiErrors = testData.mockedGetMultiErrors
   377  
   378  			client, err := prepare(config, backendMock)
   379  			testutil.Ok(t, err)
   380  			defer client.Stop()
   381  
   382  			// Replace the default gate with a counting version to allow checking the number of calls.
   383  			client.getMultiGate = newCountingGate(client.getMultiGate)
   384  
   385  			// Populate memcached with the initial items.
   386  			for _, item := range testData.initialItems {
   387  				testutil.Ok(t, client.SetAsync(item.Key, item.Value, time.Second))
   388  			}
   389  
   390  			// Wait until initial items have been added.
   391  			testutil.Ok(t, backendMock.waitItems(len(testData.initialItems)))
   392  
   393  			// Read back the items.
   394  			testutil.Equals(t, testData.expectedHits, client.GetMulti(ctx, testData.getKeys))
   395  
   396  			// Ensure the client has interacted with the backend as expected.
   397  			backendMock.lock.Lock()
   398  			defer backendMock.lock.Unlock()
   399  			testutil.Equals(t, testData.expectedGetMultiCount, backendMock.getMultiCount)
   400  
   401  			// Ensure the client has interacted with the gate as expected.
   402  			testutil.Equals(t, uint32(testData.expectedGateStartCount), client.getMultiGate.(*countingGate).Count())
   403  
   404  			// Ensure metrics are tracked.
   405  			testutil.Equals(t, float64(testData.expectedGetMultiCount), prom_testutil.ToFloat64(client.operations.WithLabelValues(opGetMulti)))
   406  			testutil.Equals(t, float64(testData.mockedGetMultiErrors), prom_testutil.ToFloat64(client.failures.WithLabelValues(opGetMulti, reasonOther)))
   407  		})
   408  	}
   409  }
   410  
   411  func TestMemcachedClient_sortKeysByServer(t *testing.T) {
   412  	config := defaultMemcachedClientConfig
   413  	config.Addresses = []string{"127.0.0.1:11211", "127.0.0.2:11211"}
   414  	backendMock := newMemcachedClientBackendMock()
   415  	selector := &mockServerSelector{
   416  		serversByKey: map[string]mockAddr{
   417  			"key1": "127.0.0.1:11211",
   418  			"key2": "127.0.0.2:11211",
   419  			"key3": "127.0.0.1:11211",
   420  			"key4": "127.0.0.2:11211",
   421  			"key5": "127.0.0.1:11211",
   422  			"key6": "127.0.0.2:11211",
   423  		},
   424  	}
   425  
   426  	client, err := newMemcachedClient(log.NewNopLogger(), backendMock, selector, config, nil, "test")
   427  	testutil.Ok(t, err)
   428  	defer client.Stop()
   429  
   430  	keys := []string{
   431  		"key1",
   432  		"key2",
   433  		"key3",
   434  		"key4",
   435  		"key5",
   436  		"key6",
   437  	}
   438  
   439  	sorted := client.sortKeysByServer(keys)
   440  	testutil.ContainsStringSlice(t, sorted, []string{"key1", "key3", "key5"})
   441  	testutil.ContainsStringSlice(t, sorted, []string{"key2", "key4", "key6"})
   442  }
   443  
   444  type mockAddr string
   445  
   446  func (m mockAddr) Network() string {
   447  	return "mock"
   448  }
   449  
   450  func (m mockAddr) String() string {
   451  	return string(m)
   452  }
   453  
   454  type mockServerSelector struct {
   455  	serversByKey map[string]mockAddr
   456  }
   457  
   458  func (m *mockServerSelector) PickServer(key string) (net.Addr, error) {
   459  	if srv, ok := m.serversByKey[key]; ok {
   460  		return srv, nil
   461  	}
   462  
   463  	panic(fmt.Sprintf("unmapped key: %s", key))
   464  }
   465  
   466  func (m *mockServerSelector) Each(f func(net.Addr) error) error {
   467  	for k := range m.serversByKey {
   468  		addr := m.serversByKey[k]
   469  		if err := f(addr); err != nil {
   470  			return err
   471  		}
   472  	}
   473  
   474  	return nil
   475  }
   476  
   477  func (m *mockServerSelector) SetServers(...string) error {
   478  	return nil
   479  }
   480  
   481  func prepare(config MemcachedClientConfig, backendMock *memcachedClientBackendMock) (*memcachedClient, error) {
   482  	logger := log.NewNopLogger()
   483  	selector := &MemcachedJumpHashSelector{}
   484  	client, err := newMemcachedClient(logger, backendMock, selector, config, nil, "test")
   485  
   486  	return client, err
   487  }
   488  
   489  type memcachedClientBackendMock struct {
   490  	lock           sync.Mutex
   491  	items          map[string]*memcache.Item
   492  	getMultiCount  int
   493  	getMultiErrors int
   494  }
   495  
   496  func newMemcachedClientBackendMock() *memcachedClientBackendMock {
   497  	return &memcachedClientBackendMock{
   498  		items: map[string]*memcache.Item{},
   499  	}
   500  }
   501  
   502  func (c *memcachedClientBackendMock) GetMulti(keys []string) (map[string]*memcache.Item, error) {
   503  	c.lock.Lock()
   504  	defer c.lock.Unlock()
   505  
   506  	c.getMultiCount++
   507  	if c.getMultiCount <= c.getMultiErrors {
   508  		return nil, errors.New("mocked GetMulti error")
   509  	}
   510  
   511  	items := make(map[string]*memcache.Item)
   512  	for _, key := range keys {
   513  		if item, ok := c.items[key]; ok {
   514  			items[key] = item
   515  		}
   516  	}
   517  
   518  	return items, nil
   519  }
   520  
   521  func (c *memcachedClientBackendMock) Set(item *memcache.Item) error {
   522  	c.lock.Lock()
   523  	defer c.lock.Unlock()
   524  
   525  	c.items[item.Key] = item
   526  
   527  	return nil
   528  }
   529  
   530  func (c *memcachedClientBackendMock) waitItems(expected int) error {
   531  	deadline := time.Now().Add(1 * time.Second)
   532  
   533  	for time.Now().Before(deadline) {
   534  		c.lock.Lock()
   535  		count := len(c.items)
   536  		c.lock.Unlock()
   537  
   538  		if count >= expected {
   539  			return nil
   540  		}
   541  	}
   542  
   543  	return errors.New("timeout expired while waiting for items in the memcached mock")
   544  }
   545  
   546  // countingGate implements gate.Gate and counts the number of times Start is called.
   547  type countingGate struct {
   548  	wrapped gate.Gate
   549  	count   *atomic.Uint32
   550  }
   551  
   552  func newCountingGate(g gate.Gate) gate.Gate {
   553  	return &countingGate{
   554  		wrapped: g,
   555  		count:   atomic.NewUint32(0),
   556  	}
   557  }
   558  
   559  func (c *countingGate) Start(ctx context.Context) error {
   560  	c.count.Inc()
   561  	return c.wrapped.Start(ctx)
   562  }
   563  
   564  func (c *countingGate) Done() {
   565  	c.wrapped.Done()
   566  }
   567  
   568  func (c *countingGate) Count() uint32 {
   569  	return c.count.Load()
   570  }
   571  
   572  func TestMultipleClientsCanUseSameRegistry(t *testing.T) {
   573  	reg := prometheus.NewRegistry()
   574  
   575  	config := defaultMemcachedClientConfig
   576  	config.Addresses = []string{"127.0.0.1:11211"}
   577  
   578  	client1, err := NewMemcachedClientWithConfig(log.NewNopLogger(), "a", config, reg)
   579  	testutil.Ok(t, err)
   580  	defer client1.Stop()
   581  
   582  	client2, err := NewMemcachedClientWithConfig(log.NewNopLogger(), "b", config, reg)
   583  	testutil.Ok(t, err)
   584  	defer client2.Stop()
   585  }
   586  
   587  func TestMemcachedClient_GetMulti_ContextCancelled(t *testing.T) {
   588  	config := defaultMemcachedClientConfig
   589  	config.Addresses = []string{"127.0.0.1:11211"}
   590  	config.MaxGetMultiBatchSize = 2
   591  	config.MaxGetMultiConcurrency = 2
   592  
   593  	// Create a new context that will be used for our "blocking" backend so that we can
   594  	// actually stop it at the end of the test and not leak goroutines.
   595  	backendCtx, backendCancel := context.WithCancel(context.Background())
   596  	defer backendCancel()
   597  
   598  	selector := &MemcachedJumpHashSelector{}
   599  	backendMock := newMemcachedClientBlockingMock(backendCtx)
   600  
   601  	client, err := newMemcachedClient(log.NewNopLogger(), backendMock, selector, config, prometheus.NewPedanticRegistry(), "test")
   602  	testutil.Ok(t, err)
   603  	defer client.Stop()
   604  
   605  	// Immediately cancel the context that will be used for the GetMulti request. This will
   606  	// ensure that the method called by the batching logic (getMultiSingle) returns immediately
   607  	// instead of calling the underlying memcached client (which blocks forever in this test).
   608  	ctx, cancel := context.WithCancel(context.Background())
   609  	cancel()
   610  
   611  	items := client.GetMulti(ctx, []string{"key1", "key2", "key3", "key4"})
   612  	testutil.Equals(t, 0, len(items))
   613  }
   614  
   615  type memcachedClientBlockingMock struct {
   616  	ctx context.Context
   617  }
   618  
   619  func newMemcachedClientBlockingMock(ctx context.Context) *memcachedClientBlockingMock {
   620  	return &memcachedClientBlockingMock{ctx: ctx}
   621  }
   622  
   623  func (c *memcachedClientBlockingMock) GetMulti([]string) (map[string]*memcache.Item, error) {
   624  	// Block until this backend client is explicitly stopped so that we can ensure the memcached
   625  	// client won't be blocked waiting for results that will never be returned.
   626  	<-c.ctx.Done()
   627  	return nil, nil
   628  }
   629  
   630  func (c *memcachedClientBlockingMock) Set(*memcache.Item) error {
   631  	return nil
   632  }