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 }