github.com/thanos-io/thanos@v0.32.5/pkg/store/cache/caching_bucket_test.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 //nolint:unparam 5 package storecache 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "sort" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/pkg/errors" 19 promtest "github.com/prometheus/client_golang/prometheus/testutil" 20 21 "github.com/thanos-io/objstore" 22 23 "github.com/efficientgo/core/testutil" 24 thanoscache "github.com/thanos-io/thanos/pkg/cache" 25 "github.com/thanos-io/thanos/pkg/runutil" 26 "github.com/thanos-io/thanos/pkg/store/cache/cachekey" 27 ) 28 29 const testFilename = "/random_object" 30 31 func TestChunksCaching(t *testing.T) { 32 length := int64(1024 * 1024) 33 subrangeSize := int64(16000) // All tests are based on this value. 34 35 data := make([]byte, length) 36 for ix := 0; ix < len(data); ix++ { 37 data[ix] = byte(ix) 38 } 39 40 name := "/test/chunks/000001" 41 42 inmem := objstore.NewInMemBucket() 43 testutil.Ok(t, inmem.Upload(context.Background(), name, bytes.NewReader(data))) 44 45 // We reuse cache between tests (!) 46 cache := newMockCache() 47 48 // Warning, these tests must be run in order, they depend cache state from previous test. 49 for _, tc := range []struct { 50 name string 51 init func() 52 offset int64 53 length int64 54 maxGetRangeRequests int 55 expectedLength int64 56 expectedFetchedBytes int64 57 expectedCachedBytes int64 58 expectedRefetchedBytes int64 59 }{ 60 { 61 name: "basic test", 62 offset: 555555, 63 length: 55555, 64 expectedLength: 55555, 65 expectedFetchedBytes: 5 * subrangeSize, 66 }, 67 68 { 69 name: "same request will hit all subranges in the cache", 70 offset: 555555, 71 length: 55555, 72 expectedLength: 55555, 73 expectedCachedBytes: 5 * subrangeSize, 74 }, 75 76 { 77 name: "request data close to the end of object", 78 offset: length - 10, 79 length: 3000, 80 expectedLength: 10, 81 expectedFetchedBytes: 8576, // Last (incomplete) subrange is fetched. 82 }, 83 84 { 85 name: "another request data close to the end of object, cached by previous test", 86 offset: 1040100, 87 length: subrangeSize, 88 expectedLength: 8476, 89 expectedCachedBytes: 8576, 90 }, 91 92 { 93 name: "entire object, combination of cached and uncached subranges", 94 offset: 0, 95 length: length, 96 expectedLength: length, 97 expectedCachedBytes: 5*subrangeSize + 8576, // 5 subrange cached from first test, plus last incomplete subrange. 98 expectedFetchedBytes: 60 * subrangeSize, 99 }, 100 101 { 102 name: "entire object again, everything is cached", 103 offset: 0, 104 length: length, 105 expectedLength: length, 106 expectedCachedBytes: length, // Entire file is now cached. 107 }, 108 109 { 110 name: "entire object again, nothing is cached", 111 offset: 0, 112 length: length, 113 expectedLength: length, 114 expectedFetchedBytes: length, 115 expectedCachedBytes: 0, // Cache is flushed. 116 init: func() { 117 cache.flush() 118 }, 119 }, 120 121 { 122 name: "missing first subranges", 123 offset: 0, 124 length: 10 * subrangeSize, 125 expectedLength: 10 * subrangeSize, 126 expectedFetchedBytes: 3 * subrangeSize, 127 expectedCachedBytes: 7 * subrangeSize, 128 init: func() { 129 // Delete first 3 subranges. 130 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 0 * subrangeSize, End: 1 * subrangeSize} 131 delete(cache.cache, objectSubrange.String()) 132 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 1 * subrangeSize, End: 2 * subrangeSize} 133 delete(cache.cache, objectSubrange.String()) 134 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 2 * subrangeSize, End: 3 * subrangeSize} 135 delete(cache.cache, objectSubrange.String()) 136 }, 137 }, 138 139 { 140 name: "missing last subranges", 141 offset: 0, 142 length: 10 * subrangeSize, 143 expectedLength: 10 * subrangeSize, 144 expectedFetchedBytes: 3 * subrangeSize, 145 expectedCachedBytes: 7 * subrangeSize, 146 init: func() { 147 // Delete last 3 subranges. 148 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 7 * subrangeSize, End: 8 * subrangeSize} 149 delete(cache.cache, objectSubrange.String()) 150 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 8 * subrangeSize, End: 9 * subrangeSize} 151 delete(cache.cache, objectSubrange.String()) 152 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 9 * subrangeSize, End: 10 * subrangeSize} 153 delete(cache.cache, objectSubrange.String()) 154 }, 155 }, 156 157 { 158 name: "missing middle subranges", 159 offset: 0, 160 length: 10 * subrangeSize, 161 expectedLength: 10 * subrangeSize, 162 expectedFetchedBytes: 3 * subrangeSize, 163 expectedCachedBytes: 7 * subrangeSize, 164 init: func() { 165 // Delete 3 subranges in the middle. 166 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 3 * subrangeSize, End: 4 * subrangeSize} 167 delete(cache.cache, objectSubrange.String()) 168 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 4 * subrangeSize, End: 5 * subrangeSize} 169 delete(cache.cache, objectSubrange.String()) 170 objectSubrange = cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: 5 * subrangeSize, End: 6 * subrangeSize} 171 delete(cache.cache, objectSubrange.String()) 172 }, 173 }, 174 175 { 176 name: "missing everything except middle subranges", 177 offset: 0, 178 length: 10 * subrangeSize, 179 expectedLength: 10 * subrangeSize, 180 expectedFetchedBytes: 7 * subrangeSize, 181 expectedCachedBytes: 3 * subrangeSize, 182 init: func() { 183 // Delete all but 3 subranges in the middle, and keep unlimited number of ranged subrequests. 184 for i := int64(0); i < 10; i++ { 185 if i > 0 && i%3 == 0 { 186 continue 187 } 188 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: i * subrangeSize, End: (i + 1) * subrangeSize} 189 delete(cache.cache, objectSubrange.String()) 190 } 191 }, 192 }, 193 194 { 195 name: "missing everything except middle subranges, one subrequest only", 196 offset: 0, 197 length: 10 * subrangeSize, 198 expectedLength: 10 * subrangeSize, 199 expectedFetchedBytes: 7 * subrangeSize, 200 expectedCachedBytes: 3 * subrangeSize, 201 expectedRefetchedBytes: 3 * subrangeSize, // Entire object fetched, 3 subranges are "refetched". 202 maxGetRangeRequests: 1, 203 init: func() { 204 // Delete all but 3 subranges in the middle, but only allow 1 subrequest. 205 for i := int64(0); i < 10; i++ { 206 if i == 3 || i == 5 || i == 7 { 207 continue 208 } 209 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: i * subrangeSize, End: (i + 1) * subrangeSize} 210 delete(cache.cache, objectSubrange.String()) 211 } 212 }, 213 }, 214 215 { 216 name: "missing everything except middle subranges, two subrequests", 217 offset: 0, 218 length: 10 * subrangeSize, 219 expectedLength: 10 * subrangeSize, 220 expectedFetchedBytes: 7 * subrangeSize, 221 expectedCachedBytes: 3 * subrangeSize, 222 maxGetRangeRequests: 2, 223 init: func() { 224 // Delete all but one subranges in the middle, and allow 2 subrequests. They will be: 0-80000, 128000-160000. 225 for i := int64(0); i < 10; i++ { 226 if i == 5 || i == 6 || i == 7 { 227 continue 228 } 229 objectSubrange := cachekey.BucketCacheKey{Verb: cachekey.SubrangeVerb, Name: name, Start: i * subrangeSize, End: (i + 1) * subrangeSize} 230 delete(cache.cache, objectSubrange.String()) 231 } 232 }, 233 }, 234 } { 235 t.Run(tc.name, func(t *testing.T) { 236 if tc.init != nil { 237 tc.init() 238 } 239 240 cfg := thanoscache.NewCachingBucketConfig() 241 cfg.CacheGetRange("chunks", cache, isTSDBChunkFile, subrangeSize, time.Hour, time.Hour, tc.maxGetRangeRequests) 242 243 cachingBucket, err := NewCachingBucket(inmem, cfg, nil, nil) 244 testutil.Ok(t, err) 245 246 verifyGetRange(t, cachingBucket, name, tc.offset, tc.length, tc.expectedLength) 247 testutil.Equals(t, tc.expectedCachedBytes, int64(promtest.ToFloat64(cachingBucket.fetchedGetRangeBytes.WithLabelValues(originCache, "chunks")))) 248 testutil.Equals(t, tc.expectedFetchedBytes, int64(promtest.ToFloat64(cachingBucket.fetchedGetRangeBytes.WithLabelValues(originBucket, "chunks")))) 249 testutil.Equals(t, tc.expectedRefetchedBytes, int64(promtest.ToFloat64(cachingBucket.refetchedGetRangeBytes.WithLabelValues(originCache, "chunks")))) 250 }) 251 } 252 } 253 254 func verifyGetRange(t *testing.T, cachingBucket *CachingBucket, name string, offset, length, expectedLength int64) { 255 r, err := cachingBucket.GetRange(context.Background(), name, offset, length) 256 testutil.Ok(t, err) 257 258 read, err := io.ReadAll(r) 259 testutil.Ok(t, err) 260 testutil.Equals(t, expectedLength, int64(len(read))) 261 262 for ix := 0; ix < len(read); ix++ { 263 if byte(ix)+byte(offset) != read[ix] { 264 t.Fatalf("bytes differ at position %d", ix) 265 } 266 } 267 } 268 269 type cacheItem struct { 270 data []byte 271 exp time.Time 272 } 273 274 type mockCache struct { 275 mu sync.Mutex 276 cache map[string]cacheItem 277 } 278 279 func newMockCache() *mockCache { 280 c := &mockCache{} 281 c.flush() 282 return c 283 } 284 285 func (m *mockCache) Store(data map[string][]byte, ttl time.Duration) { 286 m.mu.Lock() 287 defer m.mu.Unlock() 288 289 exp := time.Now().Add(ttl) 290 for key, val := range data { 291 m.cache[key] = cacheItem{data: val, exp: exp} 292 } 293 } 294 295 func (m *mockCache) Fetch(_ context.Context, keys []string) map[string][]byte { 296 m.mu.Lock() 297 defer m.mu.Unlock() 298 299 found := make(map[string][]byte, len(keys)) 300 301 now := time.Now() 302 for _, k := range keys { 303 v, ok := m.cache[k] 304 if ok && now.Before(v.exp) { 305 found[k] = v.data 306 } 307 } 308 309 return found 310 } 311 312 func (m *mockCache) Name() string { 313 return "mockCache" 314 } 315 316 func (m *mockCache) flush() { 317 m.cache = map[string]cacheItem{} 318 } 319 320 func TestMergeRanges(t *testing.T) { 321 for ix, tc := range []struct { 322 input []rng 323 limit int64 324 expected []rng 325 }{ 326 { 327 input: nil, 328 limit: 0, 329 expected: nil, 330 }, 331 332 { 333 input: []rng{{start: 0, end: 100}, {start: 100, end: 200}, {start: 500, end: 1000}}, 334 limit: 0, 335 expected: []rng{{start: 0, end: 200}, {start: 500, end: 1000}}, 336 }, 337 338 { 339 input: []rng{{start: 0, end: 100}, {start: 500, end: 1000}}, 340 limit: 300, 341 expected: []rng{{start: 0, end: 100}, {start: 500, end: 1000}}, 342 }, 343 { 344 input: []rng{{start: 0, end: 100}, {start: 500, end: 1000}}, 345 limit: 400, 346 expected: []rng{{start: 0, end: 1000}}, 347 }, 348 } { 349 t.Run(fmt.Sprintf("%d", ix), func(t *testing.T) { 350 testutil.Equals(t, tc.expected, mergeRanges(tc.input, tc.limit)) 351 }) 352 } 353 } 354 355 func TestInvalidOffsetAndLength(t *testing.T) { 356 b := &testBucket{objstore.NewInMemBucket()} 357 358 cfg := thanoscache.NewCachingBucketConfig() 359 cfg.CacheGetRange("chunks", newMockCache(), func(string) bool { return true }, 10000, time.Hour, time.Hour, 3) 360 361 c, err := NewCachingBucket(b, cfg, nil, nil) 362 testutil.Ok(t, err) 363 364 r, err := c.GetRange(context.Background(), "test", -1, 1000) 365 testutil.Equals(t, nil, r) 366 testutil.NotOk(t, err) 367 368 r, err = c.GetRange(context.Background(), "test", 100, -1) 369 testutil.Equals(t, nil, r) 370 testutil.NotOk(t, err) 371 } 372 373 type testBucket struct { 374 *objstore.InMemBucket 375 } 376 377 func (b *testBucket) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error) { 378 if off < 0 { 379 return nil, errors.Errorf("invalid offset: %d", off) 380 } 381 382 if length <= 0 { 383 return nil, errors.Errorf("invalid length: %d", length) 384 } 385 386 return b.InMemBucket.GetRange(ctx, name, off, length) 387 } 388 389 func TestCachedIter(t *testing.T) { 390 inmem := objstore.NewInMemBucket() 391 testutil.Ok(t, inmem.Upload(context.Background(), "/file-1", strings.NewReader("hej"))) 392 testutil.Ok(t, inmem.Upload(context.Background(), "/file-2", strings.NewReader("ahoj"))) 393 testutil.Ok(t, inmem.Upload(context.Background(), "/file-3", strings.NewReader("hello"))) 394 testutil.Ok(t, inmem.Upload(context.Background(), "/file-4", strings.NewReader("ciao"))) 395 396 allFiles := []string{"/file-1", "/file-2", "/file-3", "/file-4"} 397 398 // We reuse cache between tests (!) 399 cache := newMockCache() 400 401 const cfgName = "dirs" 402 cfg := thanoscache.NewCachingBucketConfig() 403 cfg.CacheIter(cfgName, cache, func(string) bool { return true }, 5*time.Minute, JSONIterCodec{}) 404 405 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 406 testutil.Ok(t, err) 407 408 verifyIter(t, cb, allFiles, false, cfgName) 409 410 testutil.Ok(t, inmem.Upload(context.Background(), "/file-5", strings.NewReader("nazdar"))) 411 verifyIter(t, cb, allFiles, true, cfgName) // Iter returns old response. 412 413 cache.flush() 414 allFiles = append(allFiles, "/file-5") 415 verifyIter(t, cb, allFiles, false, cfgName) 416 417 cache.flush() 418 419 e := errors.Errorf("test error") 420 421 // This iteration returns false. Result will not be cached. 422 testutil.Equals(t, e, cb.Iter(context.Background(), "/", func(_ string) error { 423 return e 424 })) 425 426 // Nothing cached now. 427 verifyIter(t, cb, allFiles, false, cfgName) 428 } 429 430 func verifyIter(t *testing.T, cb *CachingBucket, expectedFiles []string, expectedCache bool, cfgName string) { 431 hitsBefore := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpIter, cfgName))) 432 433 col := iterCollector{} 434 testutil.Ok(t, cb.Iter(context.Background(), "/", col.collect)) 435 436 hitsAfter := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpIter, cfgName))) 437 438 sort.Strings(col.items) 439 testutil.Equals(t, expectedFiles, col.items) 440 441 expectedHitsDiff := 0 442 if expectedCache { 443 expectedHitsDiff = 1 444 } 445 446 testutil.Equals(t, expectedHitsDiff, hitsAfter-hitsBefore) 447 } 448 449 type iterCollector struct { 450 items []string 451 } 452 453 func (it *iterCollector) collect(s string) error { 454 it.items = append(it.items, s) 455 return nil 456 } 457 458 func TestExists(t *testing.T) { 459 inmem := objstore.NewInMemBucket() 460 461 // We reuse cache between tests (!) 462 cache := newMockCache() 463 464 cfg := thanoscache.NewCachingBucketConfig() 465 const cfgName = "test" 466 cfg.CacheExists(cfgName, cache, matchAll, 10*time.Minute, 2*time.Minute) 467 468 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 469 testutil.Ok(t, err) 470 471 verifyExists(t, cb, testFilename, false, false, cfgName) 472 473 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, strings.NewReader("hej"))) 474 verifyExists(t, cb, testFilename, false, true, cfgName) // Reused cache result. 475 cache.flush() 476 verifyExists(t, cb, testFilename, true, false, cfgName) 477 478 testutil.Ok(t, inmem.Delete(context.Background(), testFilename)) 479 verifyExists(t, cb, testFilename, true, true, cfgName) // Reused cache result. 480 cache.flush() 481 verifyExists(t, cb, testFilename, false, false, cfgName) 482 } 483 484 func TestExistsCachingDisabled(t *testing.T) { 485 inmem := objstore.NewInMemBucket() 486 487 // We reuse cache between tests (!) 488 cache := newMockCache() 489 490 cfg := thanoscache.NewCachingBucketConfig() 491 const cfgName = "test" 492 cfg.CacheExists(cfgName, cache, func(string) bool { return false }, 10*time.Minute, 2*time.Minute) 493 494 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 495 testutil.Ok(t, err) 496 497 verifyExists(t, cb, testFilename, false, false, cfgName) 498 499 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, strings.NewReader("hej"))) 500 verifyExists(t, cb, testFilename, true, false, cfgName) 501 502 testutil.Ok(t, inmem.Delete(context.Background(), testFilename)) 503 verifyExists(t, cb, testFilename, false, false, cfgName) 504 } 505 506 func verifyExists(t *testing.T, cb *CachingBucket, file string, exists, fromCache bool, cfgName string) { 507 t.Helper() 508 hitsBefore := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpExists, cfgName))) 509 ok, err := cb.Exists(context.Background(), file) 510 testutil.Ok(t, err) 511 testutil.Equals(t, exists, ok) 512 hitsAfter := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpExists, cfgName))) 513 514 if fromCache { 515 testutil.Equals(t, 1, hitsAfter-hitsBefore) 516 } else { 517 testutil.Equals(t, 0, hitsAfter-hitsBefore) 518 } 519 } 520 521 func TestGet(t *testing.T) { 522 inmem := objstore.NewInMemBucket() 523 524 // We reuse cache between tests (!) 525 cache := newMockCache() 526 527 cfg := thanoscache.NewCachingBucketConfig() 528 const cfgName = "metafile" 529 cfg.CacheGet(cfgName, cache, matchAll, 1024, 10*time.Minute, 10*time.Minute, 2*time.Minute) 530 cfg.CacheExists(cfgName, cache, matchAll, 10*time.Minute, 2*time.Minute) 531 532 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 533 testutil.Ok(t, err) 534 535 verifyGet(t, cb, testFilename, nil, false, cfgName) 536 verifyExists(t, cb, testFilename, false, true, cfgName) 537 538 data := []byte("hello world") 539 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, bytes.NewBuffer(data))) 540 541 // Even if file is now uploaded, old data is served from cache. 542 verifyGet(t, cb, testFilename, nil, true, cfgName) 543 verifyExists(t, cb, testFilename, false, true, cfgName) 544 545 cache.flush() 546 547 verifyGet(t, cb, testFilename, data, false, cfgName) 548 verifyGet(t, cb, testFilename, data, true, cfgName) 549 verifyExists(t, cb, testFilename, true, true, cfgName) 550 } 551 552 func TestGetTooBigObject(t *testing.T) { 553 inmem := objstore.NewInMemBucket() 554 555 // We reuse cache between tests (!) 556 cache := newMockCache() 557 558 cfg := thanoscache.NewCachingBucketConfig() 559 const cfgName = "metafile" 560 // Only allow 5 bytes to be cached. 561 cfg.CacheGet(cfgName, cache, matchAll, 5, 10*time.Minute, 10*time.Minute, 2*time.Minute) 562 cfg.CacheExists(cfgName, cache, matchAll, 10*time.Minute, 2*time.Minute) 563 564 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 565 testutil.Ok(t, err) 566 567 data := []byte("hello world") 568 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, bytes.NewBuffer(data))) 569 570 // Object is too big, so it will not be stored to cache on first read. 571 verifyGet(t, cb, testFilename, data, false, cfgName) 572 verifyGet(t, cb, testFilename, data, false, cfgName) 573 verifyExists(t, cb, testFilename, true, true, cfgName) 574 } 575 576 func TestGetPartialRead(t *testing.T) { 577 inmem := objstore.NewInMemBucket() 578 579 cache := newMockCache() 580 581 cfg := thanoscache.NewCachingBucketConfig() 582 const cfgName = "metafile" 583 cfg.CacheGet(cfgName, cache, matchAll, 1024, 10*time.Minute, 10*time.Minute, 2*time.Minute) 584 cfg.CacheExists(cfgName, cache, matchAll, 10*time.Minute, 2*time.Minute) 585 586 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 587 testutil.Ok(t, err) 588 589 data := []byte("hello world") 590 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, bytes.NewBuffer(data))) 591 592 // Read only few bytes from data. 593 r, err := cb.Get(context.Background(), testFilename) 594 testutil.Ok(t, err) 595 _, err = r.Read(make([]byte, 1)) 596 testutil.Ok(t, err) 597 testutil.Ok(t, r.Close()) 598 599 // Object wasn't cached as it wasn't fully read. 600 verifyGet(t, cb, testFilename, data, false, cfgName) 601 // VerifyGet read object, so now it's cached. 602 verifyGet(t, cb, testFilename, data, true, cfgName) 603 } 604 605 func verifyGet(t *testing.T, cb *CachingBucket, file string, expectedData []byte, cacheUsed bool, cfgName string) { 606 hitsBefore := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpGet, cfgName))) 607 608 r, err := cb.Get(context.Background(), file) 609 if expectedData == nil { 610 testutil.Assert(t, cb.IsObjNotFoundErr(err)) 611 612 hitsAfter := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpGet, cfgName))) 613 if cacheUsed { 614 testutil.Equals(t, 1, hitsAfter-hitsBefore) 615 } else { 616 testutil.Equals(t, 0, hitsAfter-hitsBefore) 617 } 618 } else { 619 testutil.Ok(t, err) 620 defer runutil.CloseWithLogOnErr(nil, r, "verifyGet") 621 data, err := io.ReadAll(r) 622 testutil.Ok(t, err) 623 testutil.Equals(t, expectedData, data) 624 625 hitsAfter := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpGet, cfgName))) 626 if cacheUsed { 627 testutil.Equals(t, 1, hitsAfter-hitsBefore) 628 } else { 629 testutil.Equals(t, 0, hitsAfter-hitsBefore) 630 } 631 } 632 } 633 634 func TestAttributes(t *testing.T) { 635 inmem := objstore.NewInMemBucket() 636 637 // We reuse cache between tests (!) 638 cache := newMockCache() 639 640 cfg := thanoscache.NewCachingBucketConfig() 641 const cfgName = "test" 642 cfg.CacheAttributes(cfgName, cache, matchAll, time.Minute) 643 644 cb, err := NewCachingBucket(inmem, cfg, nil, nil) 645 testutil.Ok(t, err) 646 647 verifyObjectAttrs(t, cb, testFilename, -1, false, cfgName) 648 verifyObjectAttrs(t, cb, testFilename, -1, false, cfgName) // Attributes doesn't cache non-existent files. 649 650 data := []byte("hello world") 651 testutil.Ok(t, inmem.Upload(context.Background(), testFilename, bytes.NewBuffer(data))) 652 653 verifyObjectAttrs(t, cb, testFilename, len(data), false, cfgName) 654 verifyObjectAttrs(t, cb, testFilename, len(data), true, cfgName) 655 } 656 657 func verifyObjectAttrs(t *testing.T, cb *CachingBucket, file string, expectedLength int, cacheUsed bool, cfgName string) { 658 t.Helper() 659 hitsBefore := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpAttributes, cfgName))) 660 661 attrs, err := cb.Attributes(context.Background(), file) 662 if expectedLength < 0 { 663 testutil.Assert(t, cb.IsObjNotFoundErr(err)) 664 } else { 665 testutil.Ok(t, err) 666 testutil.Equals(t, int64(expectedLength), attrs.Size) 667 668 hitsAfter := int(promtest.ToFloat64(cb.operationHits.WithLabelValues(objstore.OpAttributes, cfgName))) 669 if cacheUsed { 670 testutil.Equals(t, 1, hitsAfter-hitsBefore) 671 } else { 672 testutil.Equals(t, 0, hitsAfter-hitsBefore) 673 } 674 } 675 } 676 677 func matchAll(string) bool { return true }