|
53 | 53 | import java.util.ArrayList; |
54 | 54 | import java.util.Collections; |
55 | 55 | import java.util.IdentityHashMap; |
| 56 | +import java.util.LinkedHashMap; |
56 | 57 | import java.util.List; |
57 | 58 | import java.util.Map; |
58 | 59 | import java.util.Set; |
|
62 | 63 | import java.util.concurrent.TimeUnit; |
63 | 64 | import java.util.concurrent.atomic.AtomicInteger; |
64 | 65 | import java.util.concurrent.atomic.AtomicReference; |
| 66 | +import java.util.function.Supplier; |
65 | 67 |
|
66 | 68 | import static org.hamcrest.Matchers.equalTo; |
| 69 | +import static org.hamcrest.Matchers.greaterThan; |
67 | 70 | import static org.hamcrest.Matchers.is; |
68 | 71 | import static org.hamcrest.Matchers.not; |
69 | 72 | import static org.hamcrest.Matchers.notNullValue; |
@@ -396,9 +399,9 @@ public void testCacheUnderConcurrentAccess() throws Exception { |
396 | 399 | cache.verifyInternalConsistency(); |
397 | 400 |
|
398 | 401 | // Due to cache evictions, we must get more bitsets than fields |
399 | | - assertThat(uniqueBitSets.size(), Matchers.greaterThan(FIELD_COUNT)); |
| 402 | + assertThat(uniqueBitSets.size(), greaterThan(FIELD_COUNT)); |
400 | 403 | // Due to cache evictions, we must have seen more bitsets than the cache currently holds |
401 | | - assertThat(uniqueBitSets.size(), Matchers.greaterThan(cache.entryCount())); |
| 404 | + assertThat(uniqueBitSets.size(), greaterThan(cache.entryCount())); |
402 | 405 | // Even under concurrent pressure, the cache should hit the expected size |
403 | 406 | assertThat(cache.entryCount(), is(maxCacheCount)); |
404 | 407 | assertThat(cache.ramBytesUsed(), is(maxCacheBytes)); |
@@ -517,6 +520,64 @@ public void testEquivalentMatchAllDocsQuery() { |
517 | 520 | assertFalse(DocumentSubsetBitsetCache.isEffectiveMatchAllDocsQuery(new TermQuery(new Term("term")))); |
518 | 521 | } |
519 | 522 |
|
| 523 | + public void testHitsMissesAndEvictionsStats() throws Exception { |
| 524 | + // cache that will evict all-but-one element, to test evictions |
| 525 | + final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET + (EXPECTED_BYTES_PER_BIT_SET / 2); |
| 526 | + final Settings settings = Settings.builder() |
| 527 | + .put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b") |
| 528 | + .build(); |
| 529 | + final DocumentSubsetBitsetCache cache = newCache(settings); |
| 530 | + |
| 531 | + final Supplier<Map<String, Object>> emptyStatsSupplier = () -> { |
| 532 | + final Map<String, Object> stats = new LinkedHashMap<>(); |
| 533 | + stats.put("count", 0); |
| 534 | + stats.put("memory", "0b"); |
| 535 | + stats.put("memory_in_bytes", 0L); |
| 536 | + stats.put("hits", 0L); |
| 537 | + stats.put("misses", 0L); |
| 538 | + stats.put("evictions", 0L); |
| 539 | + return stats; |
| 540 | + }; |
| 541 | + |
| 542 | + final Map<String, Object> expectedStats = emptyStatsSupplier.get(); |
| 543 | + assertThat(cache.usageStats(), equalTo(expectedStats)); |
| 544 | + |
| 545 | + runTestOnIndex((searchExecutionContext, leafContext) -> { |
| 546 | + // first lookup - miss |
| 547 | + final Query query1 = QueryBuilders.termQuery("field-1", "value-1").toQuery(searchExecutionContext); |
| 548 | + final BitSet bitSet1 = cache.getBitSet(query1, leafContext); |
| 549 | + assertThat(bitSet1, notNullValue()); |
| 550 | + expectedStats.put("count", 1); |
| 551 | + expectedStats.put("misses", 1L); |
| 552 | + expectedStats.put("memory", EXPECTED_BYTES_PER_BIT_SET + "b"); |
| 553 | + expectedStats.put("memory_in_bytes", EXPECTED_BYTES_PER_BIT_SET); |
| 554 | + assertThat(cache.usageStats(), equalTo(expectedStats)); |
| 555 | + |
| 556 | + // second same lookup - hit |
| 557 | + final BitSet bitSet1Again = cache.getBitSet(query1, leafContext); |
| 558 | + assertThat(bitSet1Again, sameInstance(bitSet1)); |
| 559 | + expectedStats.put("hits", 1L); |
| 560 | + assertThat(cache.usageStats(), equalTo(expectedStats)); |
| 561 | + |
| 562 | + // second query - miss, should evict the first one |
| 563 | + final Query query2 = QueryBuilders.termQuery("field-2", "value-2").toQuery(searchExecutionContext); |
| 564 | + final BitSet bitSet2 = cache.getBitSet(query2, leafContext); |
| 565 | + assertThat(bitSet2, notNullValue()); |
| 566 | + // surprisingly, the eviction callback can call `get` on the cache (asynchronously) which causes another miss (or hit) |
| 567 | + // so this assertion is about the current state of the code, rather than the expected or desired state. |
| 568 | + // see https://github.com/elastic/elasticsearch/issues/132842 |
| 569 | + expectedStats.put("misses", 3L); |
| 570 | + expectedStats.put("evictions", 1L); |
| 571 | + assertBusy(() -> { assertThat(cache.usageStats(), equalTo(expectedStats)); }, 200, TimeUnit.MILLISECONDS); |
| 572 | + }); |
| 573 | + |
| 574 | + final Map<String, Object> finalStats = emptyStatsSupplier.get(); |
| 575 | + finalStats.put("hits", 1L); |
| 576 | + finalStats.put("misses", 3L); |
| 577 | + finalStats.put("evictions", 2L); |
| 578 | + assertThat(cache.usageStats(), equalTo(finalStats)); |
| 579 | + } |
| 580 | + |
520 | 581 | private void runTestOnIndex(CheckedBiConsumer<SearchExecutionContext, LeafReaderContext, Exception> body) throws Exception { |
521 | 582 | runTestOnIndices(1, ctx -> { |
522 | 583 | final TestIndexContext indexContext = ctx.get(0); |
@@ -638,5 +699,4 @@ private void runTestOnIndices(int numberIndices, CheckedConsumer<List<TestIndexC |
638 | 699 | private DocumentSubsetBitsetCache newCache(Settings settings) { |
639 | 700 | return new DocumentSubsetBitsetCache(settings, singleThreadExecutor); |
640 | 701 | } |
641 | | - |
642 | 702 | } |
0 commit comments