Skip to content

Commit c8210a3

Browse files
authored
Tidy up some DLS cache code (#132416)
1 parent 63e2a3b commit c8210a3

File tree

6 files changed

+49
-68
lines changed

6 files changed

+49
-68
lines changed

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,32 +55,34 @@
5555
/**
5656
* This is a cache for {@link BitSet} instances that are used with the {@link DocumentSubsetReader}.
5757
* It is bounded by memory size and access time.
58-
*
58+
* <p>
5959
* DLS uses {@link BitSet} instances to track which documents should be visible to the user ("live") and which should not ("dead").
6060
* This means that there is a bit for each document in a Lucene index (ES shard).
6161
* Consequently, an index with 10 million document will use more than 1Mb of bitset memory for every unique DLS query, and an index
6262
* with 1 billion documents will use more than 100Mb of memory per DLS query.
6363
* Because DLS supports templating queries based on user metadata, there may be many distinct queries in use for each index, even if
6464
* there is only a single active role.
65-
*
65+
* <p>
6666
* The primary benefit of the cache is to avoid recalculating the "live docs" (visible documents) when a user performs multiple
6767
* consecutive queries across one or more large indices. Given the memory examples above, the cache is only useful if it can hold at
6868
* least 1 large (100Mb or more ) {@code BitSet} during a user's active session, and ideally should be capable of support multiple
6969
* simultaneous users with distinct DLS queries.
70-
*
70+
* <p>
7171
* For this reason the default memory usage (weight) for the cache set to 10% of JVM heap ({@link #CACHE_SIZE_SETTING}), so that it
7272
* automatically scales with the size of the Elasticsearch deployment, and can provide benefit to most use cases without needing
7373
* customisation. On a 32Gb heap, a 10% cache would be 3.2Gb which is large enough to store BitSets representing 25 billion docs.
74-
*
74+
* <p>
7575
* However, because queries can be templated by user metadata and that metadata can change frequently, it is common for the
76-
* effetively lifetime of a single DLS query to be relatively short. We do not want to sacrifice 10% of heap to a cache that is storing
77-
* BitSets that are not longer needed, so we set the TTL on this cache to be 2 hours ({@link #CACHE_TTL_SETTING}). This time has been
76+
* effective lifetime of a single DLS query to be relatively short. We do not want to sacrifice 10% of heap to a cache that is storing
77+
* BitSets that are no longer needed, so we set the TTL on this cache to be 2 hours ({@link #CACHE_TTL_SETTING}). This time has been
7878
* chosen so that it will retain BitSets that are in active use during a user's session, but not be an ongoing drain on memory.
7979
*
8080
* @see org.elasticsearch.index.cache.bitset.BitsetFilterCache
8181
*/
8282
public final class DocumentSubsetBitsetCache implements IndexReader.ClosedListener, Closeable, Accountable {
8383

84+
private static final Logger logger = LogManager.getLogger(DocumentSubsetBitsetCache.class);
85+
8486
/**
8587
* The TTL defaults to 2 hours. We default to a large cache size ({@link #CACHE_SIZE_SETTING}), and aggressively
8688
* expire unused entries so that the cache does not hold on to memory unnecessarily.
@@ -102,8 +104,6 @@ public final class DocumentSubsetBitsetCache implements IndexReader.ClosedListen
102104

103105
private static final BitSet NULL_MARKER = new FixedBitSet(0);
104106

105-
private static final Logger logger = LogManager.getLogger(DocumentSubsetBitsetCache.class);
106-
107107
/**
108108
* When a {@link BitSet} is evicted from {@link #bitsetCache}, we need to also remove it from {@link #keysByIndex}.
109109
* We use a {@link ReentrantReadWriteLock} to control atomicity here - the "read" side represents potential insertions to the
@@ -130,7 +130,8 @@ public DocumentSubsetBitsetCache(Settings settings, ThreadPool threadPool) {
130130
* @param cleanupExecutor An executor on which the cache cleanup tasks can be run. Due to the way the cache is structured internally,
131131
* it is sometimes necessary to run an asynchronous task to synchronize the internal state.
132132
*/
133-
protected DocumentSubsetBitsetCache(Settings settings, ExecutorService cleanupExecutor) {
133+
// visible for testing
134+
DocumentSubsetBitsetCache(Settings settings, ExecutorService cleanupExecutor) {
134135
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
135136
this.cacheEvictionLock = new ReleasableLock(readWriteLock.writeLock());
136137
this.cacheModificationLock = new ReleasableLock(readWriteLock.readLock());
@@ -171,7 +172,7 @@ private void onCacheEviction(RemovalNotification<BitsetCacheKey, BitSet> notific
171172
}
172173
// We push this to a background thread, so that it reduces the risk of blocking searches, but also so that the lock management is
173174
// simpler - this callback is likely to take place on a thread that is actively adding something to the cache, and is therefore
174-
// holding the read ("update") side of the lock. It is not possible to upgrade a read lock to a write ("eviction") lock, but we
175+
// holding the read ("update") side of the lock. It is not possible to upgrade a read lock to a write lock ("eviction"), but we
175176
// need to acquire that lock here.
176177
cleanupExecutor.submit(() -> {
177178
try (ReleasableLock ignored = cacheEvictionLock.acquire()) {
@@ -214,7 +215,7 @@ public long ramBytesUsed() {
214215
/**
215216
* Obtain the {@link BitSet} for the given {@code query} in the given {@code context}.
216217
* If there is a cached entry for that query and context, it will be returned.
217-
* Otherwise a new BitSet will be created and stored in the cache.
218+
* Otherwise, a new BitSet will be created and stored in the cache.
218219
* The returned BitSet may be null (e.g. if the query has no results).
219220
*/
220221
@Nullable
@@ -289,7 +290,7 @@ private static BitSet computeBitSet(Query query, LeafReaderContext context) thro
289290

290291
// Package private for testing
291292
static boolean isEffectiveMatchAllDocsQuery(Query rewrittenQuery) {
292-
if (rewrittenQuery instanceof ConstantScoreQuery && ((ConstantScoreQuery) rewrittenQuery).getQuery() instanceof MatchAllDocsQuery) {
293+
if (rewrittenQuery instanceof ConstantScoreQuery csq && csq.getQuery() instanceof MatchAllDocsQuery) {
293294
return true;
294295
}
295296
if (rewrittenQuery instanceof MatchAllDocsQuery) {
@@ -322,7 +323,8 @@ public Map<String, Object> usageStats() {
322323
return Map.of("count", entryCount(), "memory", ram.toString(), "memory_in_bytes", ram.getBytes());
323324
}
324325

325-
private static class BitsetCacheKey {
326+
private static final class BitsetCacheKey {
327+
326328
final IndexReader.CacheKey index;
327329
final Query query;
328330

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ public Bits getLiveDocs() {
195195
computeNumDocsIfNeeded();
196196
final Bits actualLiveDocs = in.getLiveDocs();
197197
if (roleQueryBits == null) {
198-
// If we would return a <code>null</code> liveDocs then that would mean that no docs are marked as deleted,
199-
// but that isn't the case. No docs match with the role query and therefore all docs are marked as deleted
198+
// If we were to return a <code>null</code> liveDocs then that would mean that no docs are marked as deleted,
199+
// but that isn't the case. No docs match with the role query and therefore all docs are marked as deleted.
200200
return new Bits.MatchNoBits(in.maxDoc());
201201
} else if (roleQueryBits instanceof MatchAllBitSet) {
202202
return actualLiveDocs;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public boolean hasStoredScript() throws IOException {
106106

107107
/**
108108
* Creates a {@link BooleanQuery} to be used as filter to restrict access to documents.<br>
109-
* Document permission queries are used to create an boolean query.<br>
109+
* Document permission queries are used to create a boolean query.<br>
110110
* If the document permissions are limited, then there is an additional filter added restricting access to documents only allowed by the
111111
* limited queries.
112112
*

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

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import java.util.Collections;
5555
import java.util.IdentityHashMap;
5656
import java.util.List;
57+
import java.util.Map;
5758
import java.util.Set;
5859
import java.util.concurrent.CountDownLatch;
5960
import java.util.concurrent.ExecutorService;
@@ -62,8 +63,6 @@
6263
import java.util.concurrent.atomic.AtomicInteger;
6364
import java.util.concurrent.atomic.AtomicReference;
6465

65-
import static java.util.Collections.emptyList;
66-
import static java.util.Collections.emptyMap;
6766
import static org.hamcrest.Matchers.equalTo;
6867
import static org.hamcrest.Matchers.is;
6968
import static org.hamcrest.Matchers.not;
@@ -525,26 +524,13 @@ private void runTestOnIndex(CheckedBiConsumer<SearchExecutionContext, LeafReader
525524
});
526525
}
527526

528-
private static final class TestIndexContext implements Closeable {
529-
private final Directory directory;
530-
private final IndexWriter indexWriter;
531-
private final DirectoryReader directoryReader;
532-
private final SearchExecutionContext searchExecutionContext;
533-
private final LeafReaderContext leafReaderContext;
534-
535-
private TestIndexContext(
536-
Directory directory,
537-
IndexWriter indexWriter,
538-
DirectoryReader directoryReader,
539-
SearchExecutionContext searchExecutionContext,
540-
LeafReaderContext leafReaderContext
541-
) {
542-
this.directory = directory;
543-
this.indexWriter = indexWriter;
544-
this.directoryReader = directoryReader;
545-
this.searchExecutionContext = searchExecutionContext;
546-
this.leafReaderContext = leafReaderContext;
547-
}
527+
private record TestIndexContext(
528+
Directory directory,
529+
IndexWriter indexWriter,
530+
DirectoryReader directoryReader,
531+
SearchExecutionContext searchExecutionContext,
532+
LeafReaderContext leafReaderContext
533+
) implements Closeable {
548534

549535
@Override
550536
public void close() throws IOException {
@@ -600,7 +586,7 @@ private TestIndexContext testIndex(MappingLookup mappingLookup, Client client) t
600586
null,
601587
() -> true,
602588
null,
603-
emptyMap(),
589+
Map.of(),
604590
MapperMetrics.NOOP
605591
);
606592

@@ -630,7 +616,7 @@ private void runTestOnIndices(int numberIndices, CheckedConsumer<List<TestIndexC
630616
types.add(new MockFieldMapper(new KeywordFieldMapper.KeywordFieldType("dne-" + i)));
631617
}
632618

633-
MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, types, emptyList());
619+
MappingLookup mappingLookup = MappingLookup.fromMappers(Mapping.EMPTY, types, List.of());
634620

635621
final Client client = mock(Client.class);
636622
when(client.settings()).thenReturn(Settings.EMPTY);

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,11 @@
6464
import java.util.Arrays;
6565
import java.util.HashSet;
6666
import java.util.List;
67+
import java.util.Map;
6768
import java.util.Set;
6869
import java.util.concurrent.Executors;
6970
import java.util.stream.Collectors;
7071

71-
import static java.util.Collections.emptyList;
72-
import static java.util.Collections.emptyMap;
73-
import static java.util.Collections.singleton;
74-
import static java.util.Collections.singletonMap;
7572
import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE;
7673
import static org.hamcrest.Matchers.containsInAnyOrder;
7774
import static org.hamcrest.Matchers.equalTo;
@@ -115,7 +112,7 @@ public void testDLS() throws Exception {
115112
null,
116113
() -> true,
117114
null,
118-
emptyMap(),
115+
Map.of(),
119116
MapperMetrics.NOOP
120117
);
121118
SearchExecutionContext searchExecutionContext = spy(realSearchExecutionContext);
@@ -154,7 +151,7 @@ public void testDLS() throws Exception {
154151
if (doc % 11 == 0) {
155152
iw.deleteDocuments(new Term("id", id));
156153
} else {
157-
if (commitAfter % commitAfter == 0) {
154+
if (doc % commitAfter == 0) {
158155
iw.commit();
159156
}
160157
valuesHitCount[valueIndex]++;
@@ -172,7 +169,7 @@ public void testDLS() throws Exception {
172169
String termQuery = "{\"term\": {\"field\": \"" + values[i] + "\"} }";
173170
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(
174171
FieldPermissions.DEFAULT,
175-
DocumentPermissions.filteredBy(singleton(new BytesArray(termQuery)))
172+
DocumentPermissions.filteredBy(Set.of(new BytesArray(termQuery)))
176173
);
177174
SecurityIndexReaderWrapper wrapper = new SecurityIndexReaderWrapper(
178175
s -> searchExecutionContext,
@@ -184,7 +181,7 @@ public void testDLS() throws Exception {
184181

185182
@Override
186183
protected IndicesAccessControl getIndicesAccessControl() {
187-
return new IndicesAccessControl(true, singletonMap("_index", indexAccessControl));
184+
return new IndicesAccessControl(true, Map.of("_index", indexAccessControl));
188185
}
189186
};
190187

@@ -237,9 +234,9 @@ public void testDLSWithLimitedPermissions() throws Exception {
237234
FieldPermissions.DEFAULT,
238235
DocumentPermissions.filteredBy(queries)
239236
);
240-
queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv21\", \"fv31\"] } }"));
237+
queries = Set.of(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv21\", \"fv31\"] } }"));
241238
if (restrictiveLimitedIndexPermissions) {
242-
queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv31\"] } }"));
239+
queries = Set.of(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv31\"] } }"));
243240
}
244241
IndicesAccessControl.IndexAccessControl limitedIndexAccessControl = new IndicesAccessControl.IndexAccessControl(
245242
FieldPermissions.DEFAULT,
@@ -271,7 +268,7 @@ public void testDLSWithLimitedPermissions() throws Exception {
271268
null,
272269
() -> true,
273270
null,
274-
emptyMap(),
271+
Map.of(),
275272
MapperMetrics.NOOP
276273
);
277274
SearchExecutionContext searchExecutionContext = spy(realSearchExecutionContext);
@@ -289,13 +286,13 @@ public void testDLSWithLimitedPermissions() throws Exception {
289286

290287
@Override
291288
protected IndicesAccessControl getIndicesAccessControl() {
292-
IndicesAccessControl indicesAccessControl = new IndicesAccessControl(true, singletonMap("_index", indexAccessControl));
289+
IndicesAccessControl indicesAccessControl = new IndicesAccessControl(true, Map.of("_index", indexAccessControl));
293290
if (noFilteredIndexPermissions) {
294291
return indicesAccessControl;
295292
}
296293
IndicesAccessControl limitedByIndicesAccessControl = new IndicesAccessControl(
297294
true,
298-
singletonMap("_index", limitedIndexAccessControl)
295+
Map.of("_index", limitedIndexAccessControl)
299296
);
300297
return indicesAccessControl.limitIndicesAccessControl(limitedByIndicesAccessControl);
301298
}
@@ -492,11 +489,7 @@ public void testDLSWithNestedDocs() throws Exception {
492489

493490
@Override
494491
protected IndicesAccessControl getIndicesAccessControl() {
495-
IndicesAccessControl indicesAccessControl = new IndicesAccessControl(
496-
true,
497-
singletonMap(indexSettings().getIndex().getName(), indexAccessControl)
498-
);
499-
return indicesAccessControl;
492+
return new IndicesAccessControl(true, Map.of(indexSettings().getIndex().getName(), indexAccessControl));
500493
}
501494
};
502495

@@ -522,6 +515,6 @@ protected IndicesAccessControl getIndicesAccessControl() {
522515

523516
private static MappingLookup createMappingLookup(List<MappedFieldType> concreteFields) {
524517
List<FieldMapper> mappers = concreteFields.stream().map(MockFieldMapper::new).collect(Collectors.toList());
525-
return MappingLookup.fromMappers(Mapping.EMPTY, mappers, emptyList());
518+
return MappingLookup.fromMappers(Mapping.EMPTY, mappers, List.of());
526519
}
527520
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030
import org.junit.After;
3131
import org.junit.Before;
3232

33-
import java.util.Arrays;
3433
import java.util.Collections;
3534
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Map;
3637
import java.util.Set;
3738

38-
import static java.util.Collections.singletonMap;
3939
import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE;
4040
import static org.hamcrest.Matchers.is;
4141
import static org.hamcrest.Matchers.sameInstance;
@@ -85,15 +85,15 @@ public void tearDown() throws Exception {
8585
esIn.close();
8686
}
8787

88-
public void testDefaultMetaFields() throws Exception {
88+
public void testDefaultMetaFields() {
8989
securityIndexReaderWrapper = new SecurityIndexReaderWrapper(null, null, securityContext, licenseState, scriptService) {
9090
@Override
9191
protected IndicesAccessControl getIndicesAccessControl() {
9292
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(
9393
new FieldPermissions(fieldPermissionDef(new String[] {}, null)),
9494
DocumentPermissions.allowAll()
9595
);
96-
return new IndicesAccessControl(true, singletonMap("_index", indexAccessControl));
96+
return new IndicesAccessControl(true, Map.of("_index", indexAccessControl));
9797
}
9898
};
9999

@@ -115,22 +115,22 @@ protected IndicesAccessControl getIndicesAccessControl() {
115115
assertThat(result.getFilter().run("some_random_regular_field"), is(false));
116116
}
117117

118-
public void testWrapReaderWhenFeatureDisabled() throws Exception {
118+
public void testWrapReaderWhenFeatureDisabled() {
119119
when(licenseState.isAllowed(DOCUMENT_LEVEL_SECURITY_FEATURE)).thenReturn(false);
120120
securityIndexReaderWrapper = new SecurityIndexReaderWrapper(null, null, securityContext, licenseState, scriptService);
121121
DirectoryReader reader = securityIndexReaderWrapper.apply(esIn);
122122
assertThat(reader, sameInstance(esIn));
123123
}
124124

125-
public void testWildcards() throws Exception {
125+
public void testWildcards() {
126126
Set<String> expected = new HashSet<>(META_FIELDS);
127127
expected.add("field1_a");
128128
expected.add("field1_b");
129129
expected.add("field1_c");
130130
assertResolved(new FieldPermissions(fieldPermissionDef(new String[] { "field1*" }, null)), expected, "field", "field2");
131131
}
132132

133-
public void testDotNotion() throws Exception {
133+
public void testDotNotion() {
134134
Set<String> expected = new HashSet<>(META_FIELDS);
135135
expected.add("foo.bar");
136136
assertResolved(new FieldPermissions(fieldPermissionDef(new String[] { "foo.bar" }, null)), expected, "foo", "foo.baz", "bar.foo");
@@ -149,7 +149,7 @@ private void assertResolved(FieldPermissions permissions, Set<String> expected,
149149
}
150150
}
151151

152-
public void testFieldPermissionsWithFieldExceptions() throws Exception {
152+
public void testFieldPermissionsWithFieldExceptions() {
153153
securityIndexReaderWrapper = new SecurityIndexReaderWrapper(null, null, securityContext, licenseState, null);
154154
String[] grantedFields = new String[] {};
155155
String[] deniedFields;
@@ -166,7 +166,7 @@ public void testFieldPermissionsWithFieldExceptions() throws Exception {
166166
deniedFields = META_FIELDS.toArray(new String[0]);
167167
assertResolved(
168168
new FieldPermissions(fieldPermissionDef(null, deniedFields)),
169-
new HashSet<>(Arrays.asList("foo", "bar", "_some_plugin_meta_field"))
169+
new HashSet<>(List.of("foo", "bar", "_some_plugin_meta_field"))
170170
);
171171

172172
// check we can add all fields with *

0 commit comments

Comments
 (0)