Skip to content

Commit ca5d778

Browse files
authored
Expose existing DLS cache x-pack usage statistics (#132845)
- add already-tracked DLS cache stats into `/xpack/usage?filter_path=security.roles.dls`
1 parent ab0fd43 commit ca5d778

File tree

3 files changed

+80
-4
lines changed

3 files changed

+80
-4
lines changed

docs/changelog/132845.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 132845
2+
summary: Expose existing DLS cache x-pack usage statistics
3+
area: Authorization
4+
type: enhancement
5+
issues: []

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCache.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040

4141
import java.io.Closeable;
4242
import java.io.IOException;
43+
import java.util.Collections;
44+
import java.util.LinkedHashMap;
4345
import java.util.List;
4446
import java.util.Map;
4547
import java.util.Objects;
@@ -320,7 +322,16 @@ public static List<Setting<?>> getSettings() {
320322

321323
public Map<String, Object> usageStats() {
322324
final ByteSizeValue ram = ByteSizeValue.ofBytes(ramBytesUsed());
323-
return Map.of("count", entryCount(), "memory", ram.toString(), "memory_in_bytes", ram.getBytes());
325+
final Cache.Stats cacheStats = bitsetCache.stats();
326+
327+
final Map<String, Object> stats = new LinkedHashMap<>();
328+
stats.put("count", entryCount());
329+
stats.put("memory", ram.toString());
330+
stats.put("memory_in_bytes", ram.getBytes());
331+
stats.put("hits", cacheStats.getHits());
332+
stats.put("misses", cacheStats.getMisses());
333+
stats.put("evictions", cacheStats.getEvictions());
334+
return Collections.unmodifiableMap(stats);
324335
}
325336

326337
private static final class BitsetCacheKey {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCacheTests.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.util.ArrayList;
5454
import java.util.Collections;
5555
import java.util.IdentityHashMap;
56+
import java.util.LinkedHashMap;
5657
import java.util.List;
5758
import java.util.Map;
5859
import java.util.Set;
@@ -62,8 +63,10 @@
6263
import java.util.concurrent.TimeUnit;
6364
import java.util.concurrent.atomic.AtomicInteger;
6465
import java.util.concurrent.atomic.AtomicReference;
66+
import java.util.function.Supplier;
6567

6668
import static org.hamcrest.Matchers.equalTo;
69+
import static org.hamcrest.Matchers.greaterThan;
6770
import static org.hamcrest.Matchers.is;
6871
import static org.hamcrest.Matchers.not;
6972
import static org.hamcrest.Matchers.notNullValue;
@@ -396,9 +399,9 @@ public void testCacheUnderConcurrentAccess() throws Exception {
396399
cache.verifyInternalConsistency();
397400

398401
// 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));
400403
// 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()));
402405
// Even under concurrent pressure, the cache should hit the expected size
403406
assertThat(cache.entryCount(), is(maxCacheCount));
404407
assertThat(cache.ramBytesUsed(), is(maxCacheBytes));
@@ -517,6 +520,64 @@ public void testEquivalentMatchAllDocsQuery() {
517520
assertFalse(DocumentSubsetBitsetCache.isEffectiveMatchAllDocsQuery(new TermQuery(new Term("term"))));
518521
}
519522

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+
520581
private void runTestOnIndex(CheckedBiConsumer<SearchExecutionContext, LeafReaderContext, Exception> body) throws Exception {
521582
runTestOnIndices(1, ctx -> {
522583
final TestIndexContext indexContext = ctx.get(0);
@@ -638,5 +699,4 @@ private void runTestOnIndices(int numberIndices, CheckedConsumer<List<TestIndexC
638699
private DocumentSubsetBitsetCache newCache(Settings settings) {
639700
return new DocumentSubsetBitsetCache(settings, singleThreadExecutor);
640701
}
641-
642702
}

0 commit comments

Comments
 (0)