|
26 | 26 | import org.apache.lucene.search.ScorerSupplier;
|
27 | 27 | import org.apache.lucene.search.Weight;
|
28 | 28 | import org.apache.lucene.store.Directory;
|
| 29 | +import org.apache.lucene.util.Accountable; |
29 | 30 | import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader;
|
30 | 31 | import org.elasticsearch.common.settings.Settings;
|
31 | 32 | import org.elasticsearch.core.IOUtils;
|
|
34 | 35 | import org.elasticsearch.index.shard.ShardId;
|
35 | 36 | import org.elasticsearch.test.ESTestCase;
|
36 | 37 |
|
| 38 | +import java.io.Closeable; |
37 | 39 | import java.io.IOException;
|
| 40 | +import java.util.ArrayList; |
| 41 | +import java.util.List; |
| 42 | +import java.util.concurrent.atomic.AtomicReference; |
| 43 | + |
| 44 | +import static org.hamcrest.Matchers.closeTo; |
| 45 | +import static org.hamcrest.Matchers.equalTo; |
| 46 | +import static org.hamcrest.Matchers.lessThan; |
38 | 47 |
|
39 | 48 | public class IndicesQueryCacheTests extends ESTestCase {
|
40 | 49 |
|
41 |
| - private static class DummyQuery extends Query { |
| 50 | + private static class DummyQuery extends Query implements Accountable { |
42 | 51 |
|
43 |
| - private final int id; |
| 52 | + private final String id; |
| 53 | + private final long sizeInCache; |
44 | 54 |
|
45 | 55 | DummyQuery(int id) {
|
| 56 | + this(Integer.toString(id), 10); |
| 57 | + } |
| 58 | + |
| 59 | + DummyQuery(String id) { |
| 60 | + this(id, 10); |
| 61 | + } |
| 62 | + |
| 63 | + DummyQuery(String id, long sizeInCache) { |
46 | 64 | this.id = id;
|
| 65 | + this.sizeInCache = sizeInCache; |
47 | 66 | }
|
48 | 67 |
|
49 | 68 | @Override
|
50 | 69 | public boolean equals(Object obj) {
|
51 |
| - return sameClassAs(obj) && id == ((DummyQuery) obj).id; |
| 70 | + return sameClassAs(obj) && id.equals(((DummyQuery) obj).id); |
52 | 71 | }
|
53 | 72 |
|
54 | 73 | @Override
|
55 | 74 | public int hashCode() {
|
56 |
| - return 31 * classHash() + id; |
| 75 | + return 31 * classHash() + id.hashCode(); |
57 | 76 | }
|
58 | 77 |
|
59 | 78 | @Override
|
@@ -82,6 +101,10 @@ public boolean isCacheable(LeafReaderContext ctx) {
|
82 | 101 | };
|
83 | 102 | }
|
84 | 103 |
|
| 104 | + @Override |
| 105 | + public long ramBytesUsed() { |
| 106 | + return sizeInCache; |
| 107 | + } |
85 | 108 | }
|
86 | 109 |
|
87 | 110 | public void testBasics() throws IOException {
|
@@ -404,4 +427,155 @@ public void testDelegatesScorerSupplier() throws Exception {
|
404 | 427 | cache.onClose(shard);
|
405 | 428 | cache.close();
|
406 | 429 | }
|
| 430 | + |
| 431 | + public void testGetStatsMemory() throws Exception { |
| 432 | + /* |
| 433 | + * This test creates 2 shards, one with two segments and one with one. It makes unique queries against all 3 segments (so that each |
| 434 | + * query will be cached, up to the max cache size), and then asserts various things about the cache memory. Most importantly, it |
| 435 | + * asserts that the memory the cache attributes to each shard is proportional to the number of segment-queries for the shard in the |
| 436 | + * cache (and not to the number of documents in the query). |
| 437 | + */ |
| 438 | + String indexName = randomIdentifier(); |
| 439 | + String uuid = randomUUID(); |
| 440 | + ShardId shard1 = new ShardId(indexName, uuid, 0); |
| 441 | + ShardId shard2 = new ShardId(indexName, uuid, 1); |
| 442 | + List<Closeable> closeableList = new ArrayList<>(); |
| 443 | + // We're going to create 2 segments for shard1, and 1 segment for shard2: |
| 444 | + int shard1Segment1Docs = randomIntBetween(11, 1000); |
| 445 | + int shard1Segment2Docs = randomIntBetween(1, 10); |
| 446 | + int shard2Segment1Docs = randomIntBetween(1, 10); |
| 447 | + IndexSearcher shard1Segment1Searcher = initializeSegment(shard1, shard1Segment1Docs, closeableList); |
| 448 | + IndexSearcher shard1Segment2Searcher = initializeSegment(shard1, shard1Segment2Docs, closeableList); |
| 449 | + IndexSearcher shard2Searcher = initializeSegment(shard2, shard2Segment1Docs, closeableList); |
| 450 | + |
| 451 | + final int maxCacheSize = 200; |
| 452 | + Settings settings = Settings.builder() |
| 453 | + .put(IndicesQueryCache.INDICES_CACHE_QUERY_COUNT_SETTING.getKey(), maxCacheSize) |
| 454 | + .put(IndicesQueryCache.INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING.getKey(), true) |
| 455 | + .build(); |
| 456 | + IndicesQueryCache cache = new IndicesQueryCache(settings); |
| 457 | + shard1Segment1Searcher.setQueryCache(cache); |
| 458 | + shard1Segment2Searcher.setQueryCache(cache); |
| 459 | + shard2Searcher.setQueryCache(cache); |
| 460 | + |
| 461 | + assertEquals(0L, cache.getStats(shard1).getMemorySizeInBytes()); |
| 462 | + |
| 463 | + final long largeQuerySize = randomIntBetween(100, 1000); |
| 464 | + final long smallQuerySize = randomIntBetween(10, 50); |
| 465 | + |
| 466 | + final int shard1Queries = randomIntBetween(20, 50); |
| 467 | + final int shard2Queries = randomIntBetween(5, 10); |
| 468 | + |
| 469 | + for (int i = 0; i < shard1Queries; ++i) { |
| 470 | + shard1Segment1Searcher.count(new DummyQuery("ingest1-" + i, largeQuerySize)); |
| 471 | + } |
| 472 | + long shard1Segment1CacheMemory = calculateActualCacheMemoryForSegment(shard1Queries, largeQuerySize, shard1Segment1Docs); |
| 473 | + assertThat(cache.getStats(shard1).getMemorySizeInBytes(), equalTo(shard1Segment1CacheMemory)); |
| 474 | + assertThat(cache.getStats(shard2).getMemorySizeInBytes(), equalTo(0L)); |
| 475 | + for (int i = 0; i < shard2Queries; ++i) { |
| 476 | + shard2Searcher.count(new DummyQuery("ingest2-" + i, smallQuerySize)); |
| 477 | + } |
| 478 | + /* |
| 479 | + * Now that we have cached some smaller things for shard2, the cache memory for shard1 has gone down. This is expected because we |
| 480 | + * report cache memory proportional to the number of segments for each shard, ignoring the number of documents or the actual |
| 481 | + * document sizes. Since the shard2 requests were smaller, the average cache memory size per segment has now gone down. |
| 482 | + */ |
| 483 | + assertThat(cache.getStats(shard1).getMemorySizeInBytes(), lessThan(shard1Segment1CacheMemory)); |
| 484 | + long shard1CacheBytes = cache.getStats(shard1).getMemorySizeInBytes(); |
| 485 | + long shard2CacheBytes = cache.getStats(shard2).getMemorySizeInBytes(); |
| 486 | + long shard2Segment1CacheMemory = calculateActualCacheMemoryForSegment(shard2Queries, smallQuerySize, shard2Segment1Docs); |
| 487 | + |
| 488 | + long totalMemory = shard1Segment1CacheMemory + shard2Segment1CacheMemory; |
| 489 | + // Each shard has some fixed overhead that we need to account for: |
| 490 | + long shard1Overhead = calculateOverheadForSegment(shard1Queries, shard1Segment1Docs); |
| 491 | + long shard2Overhead = calculateOverheadForSegment(shard2Queries, shard2Segment1Docs); |
| 492 | + long totalMemoryMinusOverhead = totalMemory - (shard1Overhead + shard2Overhead); |
| 493 | + /* |
| 494 | + * Note that the expected amount of memory we're calculating is based on the proportion of the number of queries to each shard |
| 495 | + * (since each shard currently only has queries to one segment) |
| 496 | + */ |
| 497 | + double shard1Segment1CacheMemoryShare = ((double) shard1Queries / (shard1Queries + shard2Queries)) * (totalMemoryMinusOverhead) |
| 498 | + + shard1Overhead; |
| 499 | + double shard2Segment1CacheMemoryShare = ((double) shard2Queries / (shard1Queries + shard2Queries)) * (totalMemoryMinusOverhead) |
| 500 | + + shard2Overhead; |
| 501 | + assertThat((double) shard1CacheBytes, closeTo(shard1Segment1CacheMemoryShare, 1)); // accounting for rounding |
| 502 | + assertThat((double) shard2CacheBytes, closeTo(shard2Segment1CacheMemoryShare, 1)); // accounting for rounding |
| 503 | + |
| 504 | + // Now we cache just more "big" searches on shard1, but on a different segment: |
| 505 | + for (int i = 0; i < shard1Queries; ++i) { |
| 506 | + shard1Segment2Searcher.count(new DummyQuery("ingest3-" + i, largeQuerySize)); |
| 507 | + } |
| 508 | + long shard1Segment2CacheMemory = calculateActualCacheMemoryForSegment(shard1Queries, largeQuerySize, shard1Segment2Docs); |
| 509 | + totalMemory = shard1Segment1CacheMemory + shard2Segment1CacheMemory + shard1Segment2CacheMemory; |
| 510 | + // Each shard has some fixed overhead that we need to account for: |
| 511 | + shard1Overhead = shard1Overhead + calculateOverheadForSegment(shard1Queries, shard1Segment2Docs); |
| 512 | + totalMemoryMinusOverhead = totalMemory - (shard1Overhead + shard2Overhead); |
| 513 | + /* |
| 514 | + * Note that the expected amount of memory we're calculating is based on the proportion of the number of queries to each segment. |
| 515 | + * The number of documents and the size of documents is irrelevant (aside from computing the fixed overhead). |
| 516 | + */ |
| 517 | + shard1Segment1CacheMemoryShare = ((double) (2 * shard1Queries) / ((2 * shard1Queries) + shard2Queries)) * (totalMemoryMinusOverhead) |
| 518 | + + shard1Overhead; |
| 519 | + shard1CacheBytes = cache.getStats(shard1).getMemorySizeInBytes(); |
| 520 | + assertThat((double) shard1CacheBytes, closeTo(shard1Segment1CacheMemoryShare, 1)); // accounting for rounding |
| 521 | + |
| 522 | + // Now make sure the cache only has items for shard2: |
| 523 | + for (int i = 0; i < (maxCacheSize * 2); ++i) { |
| 524 | + shard2Searcher.count(new DummyQuery("ingest4-" + i, smallQuerySize)); |
| 525 | + } |
| 526 | + assertThat(cache.getStats(shard1).getMemorySizeInBytes(), equalTo(0L)); |
| 527 | + assertThat( |
| 528 | + cache.getStats(shard2).getMemorySizeInBytes(), |
| 529 | + equalTo(calculateActualCacheMemoryForSegment(maxCacheSize, smallQuerySize, shard2Segment1Docs)) |
| 530 | + ); |
| 531 | + |
| 532 | + IOUtils.close(closeableList); |
| 533 | + cache.onClose(shard1); |
| 534 | + cache.onClose(shard2); |
| 535 | + cache.close(); |
| 536 | + } |
| 537 | + |
| 538 | + /* |
| 539 | + * This calculates the memory that actually used by a segment in the IndicesQueryCache. It assumes queryCount queries are made to the |
| 540 | + * segment, and query is querySize bytes in size. It assumes that the shard contains numDocs documents. |
| 541 | + */ |
| 542 | + private long calculateActualCacheMemoryForSegment(long queryCount, long querySize, long numDocs) { |
| 543 | + return (queryCount * (querySize + 24)) + calculateOverheadForSegment(queryCount, numDocs); |
| 544 | + } |
| 545 | + |
| 546 | + /* |
| 547 | + * This computes the part of the recorded IndicesQueryCache memory that is assigned to a segment and *not* divided up proportionally |
| 548 | + * when the cache reports the memory usage of each shard. |
| 549 | + */ |
| 550 | + private long calculateOverheadForSegment(long queryCount, long numDocs) { |
| 551 | + return queryCount * (112 + (8 * ((numDocs - 1) / 64))); |
| 552 | + } |
| 553 | + |
| 554 | + /* |
| 555 | + * This returns an IndexSearcher for a single new segment in the given shard. |
| 556 | + */ |
| 557 | + private IndexSearcher initializeSegment(ShardId shard, int numDocs, List<Closeable> closeableList) throws Exception { |
| 558 | + AtomicReference<IndexSearcher> indexSearcherReference = new AtomicReference<>(); |
| 559 | + /* |
| 560 | + * Usually creating an IndexWriter like this results in a single segment getting created, but sometimes it results in more. For the |
| 561 | + * sake of keeping the calculations in this test simple we want just a single segment. So we do this in an assertBusy. |
| 562 | + */ |
| 563 | + assertBusy(() -> { |
| 564 | + Directory dir = newDirectory(); |
| 565 | + IndexWriter indexWriter = new IndexWriter(dir, newIndexWriterConfig()); |
| 566 | + for (int i = 0; i < numDocs; i++) { |
| 567 | + indexWriter.addDocument(new Document()); |
| 568 | + } |
| 569 | + DirectoryReader directoryReader = DirectoryReader.open(indexWriter); |
| 570 | + indexWriter.close(); |
| 571 | + directoryReader = ElasticsearchDirectoryReader.wrap(directoryReader, shard); |
| 572 | + IndexSearcher indexSearcher = new IndexSearcher(directoryReader); |
| 573 | + indexSearcherReference.set(indexSearcher); |
| 574 | + indexSearcher.setQueryCachingPolicy(TrivialQueryCachingPolicy.ALWAYS); |
| 575 | + closeableList.add(directoryReader); |
| 576 | + closeableList.add(dir); |
| 577 | + assertThat(indexSearcher.getLeafContexts().size(), equalTo(1)); |
| 578 | + }); |
| 579 | + return indexSearcherReference.get(); |
| 580 | + } |
407 | 581 | }
|
0 commit comments