Skip to content

Commit 523b3d9

Browse files
authored
Avoid potential unsupported operation exception in doc bitset cache (#91490) (#91535)
This PR replaces Set.of() with explicit null handling so that remove does not get called against an immutableSet.
1 parent b8d8227 commit 523b3d9

File tree

3 files changed

+81
-35
lines changed

3 files changed

+81
-35
lines changed

docs/changelog/91490.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 91490
2+
summary: Avoid potential unsupported operation exception in doc bitset cache
3+
area: Authorization
4+
type: bug
5+
issues: []

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.util.List;
4444
import java.util.Map;
4545
import java.util.Objects;
46+
import java.util.Optional;
4647
import java.util.Set;
4748
import java.util.concurrent.ConcurrentHashMap;
4849
import java.util.concurrent.ExecutionException;
@@ -179,7 +180,7 @@ private void onCacheEviction(RemovalNotification<BitsetCacheKey, BitSet> notific
179180
// it's possible for the key to be back in the cache if it was immediately repopulated after it was evicted, so check
180181
if (bitsetCache.get(bitsetKey) == null) {
181182
// key is no longer in the cache, make sure it is no longer in the lookup map either.
182-
keysByIndex.getOrDefault(indexKey, Set.of()).remove(bitsetKey);
183+
Optional.ofNullable(keysByIndex.get(indexKey)).ifPresent(set -> set.remove(bitsetKey));
183184
}
184185
}
185186
});

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

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.apache.lucene.document.Field;
1616
import org.apache.lucene.document.StringField;
1717
import org.apache.lucene.index.DirectoryReader;
18+
import org.apache.lucene.index.IndexReader;
1819
import org.apache.lucene.index.IndexWriter;
1920
import org.apache.lucene.index.IndexWriterConfig;
2021
import org.apache.lucene.index.LeafReaderContext;
@@ -83,6 +84,9 @@
8384
public class DocumentSubsetBitsetCacheTests extends ESTestCase {
8485

8586
private static final int FIELD_COUNT = 10;
87+
// This value is based on the internal implementation details of lucene's FixedBitSet
88+
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
89+
private static final long EXPECTED_BYTES_PER_BIT_SET = 56;
8690
private ExecutorService singleThreadExecutor;
8791

8892
@Before
@@ -137,12 +141,8 @@ public void testNullEntriesAreNotCountedInMemoryUsage() throws Exception {
137141
}
138142

139143
public void testCacheRespectsMemoryLimit() throws Exception {
140-
// This value is based on the internal implementation details of lucene's FixedBitSet
141-
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
142-
final long expectedBytesPerBitSet = 56;
143-
144144
// Enough to hold exactly 2 bit-sets in the cache
145-
final long maxCacheBytes = expectedBytesPerBitSet * 2;
145+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET * 2;
146146
final Settings settings = Settings.builder()
147147
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
148148
.build();
@@ -158,29 +158,29 @@ public void testCacheRespectsMemoryLimit() throws Exception {
158158
final Query query = queryBuilder.toQuery(searchExecutionContext);
159159
final BitSet bitSet = cache.getBitSet(query, leafContext);
160160
assertThat(bitSet, notNullValue());
161-
assertThat(bitSet.ramBytesUsed(), equalTo(expectedBytesPerBitSet));
161+
assertThat(bitSet.ramBytesUsed(), equalTo(EXPECTED_BYTES_PER_BIT_SET));
162162

163163
// The first time through we have 1 entry, after that we have 2
164164
final int expectedCount = i == 1 ? 1 : 2;
165165
assertThat(cache.entryCount(), equalTo(expectedCount));
166-
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * expectedBytesPerBitSet));
166+
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * EXPECTED_BYTES_PER_BIT_SET));
167167

168168
// Older queries should get evicted, but the query from last iteration should still be cached
169169
if (previousQuery != null) {
170170
assertThat(cache.getBitSet(previousQuery, leafContext), sameInstance(previousBitSet));
171171
assertThat(cache.entryCount(), equalTo(expectedCount));
172-
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * expectedBytesPerBitSet));
172+
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * EXPECTED_BYTES_PER_BIT_SET));
173173
}
174174
previousQuery = query;
175175
previousBitSet = bitSet;
176176

177177
assertThat(cache.getBitSet(queryBuilder.toQuery(searchExecutionContext), leafContext), sameInstance(bitSet));
178178
assertThat(cache.entryCount(), equalTo(expectedCount));
179-
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * expectedBytesPerBitSet));
179+
assertThat(cache.ramBytesUsed(), equalTo(expectedCount * EXPECTED_BYTES_PER_BIT_SET));
180180
}
181181

182182
assertThat(cache.entryCount(), equalTo(2));
183-
assertThat(cache.ramBytesUsed(), equalTo(2 * expectedBytesPerBitSet));
183+
assertThat(cache.ramBytesUsed(), equalTo(2 * EXPECTED_BYTES_PER_BIT_SET));
184184

185185
cache.clear("testing");
186186

@@ -190,12 +190,8 @@ public void testCacheRespectsMemoryLimit() throws Exception {
190190
}
191191

192192
public void testLogWarningIfBitSetExceedsCacheSize() throws Exception {
193-
// This value is based on the internal implementation details of lucene's FixedBitSet
194-
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
195-
final long expectedBytesPerBitSet = 56;
196-
197193
// Enough to hold less than 1 bit-sets in the cache
198-
final long maxCacheBytes = expectedBytesPerBitSet - expectedBytesPerBitSet / 3;
194+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET - EXPECTED_BYTES_PER_BIT_SET / 3;
199195
final Settings settings = Settings.builder()
200196
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
201197
.build();
@@ -214,7 +210,7 @@ public void testLogWarningIfBitSetExceedsCacheSize() throws Exception {
214210
cache.getClass().getName(),
215211
Level.WARN,
216212
"built a DLS BitSet that uses ["
217-
+ expectedBytesPerBitSet
213+
+ EXPECTED_BYTES_PER_BIT_SET
218214
+ "] bytes; the DLS BitSet cache has a maximum size of ["
219215
+ maxCacheBytes
220216
+ "] bytes; this object cannot be cached and will need to be rebuilt for each use;"
@@ -227,7 +223,7 @@ public void testLogWarningIfBitSetExceedsCacheSize() throws Exception {
227223
final Query query = queryBuilder.toQuery(searchExecutionContext);
228224
final BitSet bitSet = cache.getBitSet(query, leafContext);
229225
assertThat(bitSet, notNullValue());
230-
assertThat(bitSet.ramBytesUsed(), equalTo(expectedBytesPerBitSet));
226+
assertThat(bitSet.ramBytesUsed(), equalTo(EXPECTED_BYTES_PER_BIT_SET));
231227
});
232228

233229
mockAppender.assertAllExpectationsMatched();
@@ -238,12 +234,8 @@ public void testLogWarningIfBitSetExceedsCacheSize() throws Exception {
238234
}
239235

240236
public void testLogMessageIfCacheFull() throws Exception {
241-
// This value is based on the internal implementation details of lucene's FixedBitSet
242-
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
243-
final long expectedBytesPerBitSet = 56;
244-
245237
// Enough to hold slightly more than 1 bit-sets in the cache
246-
final long maxCacheBytes = expectedBytesPerBitSet + expectedBytesPerBitSet / 3;
238+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET + EXPECTED_BYTES_PER_BIT_SET / 3;
247239
final Settings settings = Settings.builder()
248240
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
249241
.build();
@@ -272,7 +264,7 @@ public void testLogMessageIfCacheFull() throws Exception {
272264
final Query query = queryBuilder.toQuery(searchExecutionContext);
273265
final BitSet bitSet = cache.getBitSet(query, leafContext);
274266
assertThat(bitSet, notNullValue());
275-
assertThat(bitSet.ramBytesUsed(), equalTo(expectedBytesPerBitSet));
267+
assertThat(bitSet.ramBytesUsed(), equalTo(EXPECTED_BYTES_PER_BIT_SET));
276268
}
277269
});
278270

@@ -311,12 +303,8 @@ public void testCacheRespectsAccessTimeExpiry() throws Exception {
311303
}
312304

313305
public void testIndexLookupIsClearedWhenBitSetIsEvicted() throws Exception {
314-
// This value is based on the internal implementation details of lucene's FixedBitSet
315-
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
316-
final long expectedBytesPerBitSet = 56;
317-
318306
// Enough to hold slightly more than 1 bit-set in the cache
319-
final long maxCacheBytes = expectedBytesPerBitSet + expectedBytesPerBitSet / 2;
307+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET + EXPECTED_BYTES_PER_BIT_SET / 2;
320308
final Settings settings = Settings.builder()
321309
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
322310
.build();
@@ -360,16 +348,12 @@ public void testIndexLookupIsClearedWhenBitSetIsEvicted() throws Exception {
360348
}
361349

362350
public void testCacheUnderConcurrentAccess() throws Exception {
363-
// This value is based on the internal implementation details of lucene's FixedBitSet
364-
// If the implementation changes, this can be safely updated to match the new ram usage for a single bitset
365-
final long expectedBytesPerBitSet = 56;
366-
367351
final int concurrentThreads = randomIntBetween(5, 8);
368352
final int numberOfIndices = randomIntBetween(3, 8);
369353

370354
// Force cache evictions by setting the size to be less than the number of distinct queries we search on.
371355
final int maxCacheCount = randomIntBetween(FIELD_COUNT / 2, FIELD_COUNT * 3 / 4);
372-
final long maxCacheBytes = expectedBytesPerBitSet * maxCacheCount;
356+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET * maxCacheCount;
373357
final Settings settings = Settings.builder()
374358
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
375359
.build();
@@ -412,7 +396,7 @@ public void testCacheUnderConcurrentAccess() throws Exception {
412396
final Query query = queryBuilder.toQuery(randomContext.searchExecutionContext);
413397
final BitSet bitSet = cache.getBitSet(query, randomContext.leafReaderContext);
414398
assertThat(bitSet, notNullValue());
415-
assertThat(bitSet.ramBytesUsed(), equalTo(expectedBytesPerBitSet));
399+
assertThat(bitSet.ramBytesUsed(), equalTo(EXPECTED_BYTES_PER_BIT_SET));
416400
uniqueBitSets.add(bitSet);
417401
}
418402
}
@@ -446,6 +430,62 @@ public void testCacheUnderConcurrentAccess() throws Exception {
446430
}
447431
}
448432

433+
public void testCleanupWorksWhenIndexIsClosing() throws Exception {
434+
// Enough to hold slightly more than 1 bit-set in the cache
435+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET + EXPECTED_BYTES_PER_BIT_SET / 2;
436+
final Settings settings = Settings.builder()
437+
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
438+
.build();
439+
final ExecutorService threads = Executors.newFixedThreadPool(1);
440+
final ExecutorService cleanupExecutor = Mockito.mock(ExecutorService.class);
441+
final CountDownLatch cleanupReadyLatch = new CountDownLatch(1);
442+
final CountDownLatch cleanupCompleteLatch = new CountDownLatch(1);
443+
final CountDownLatch indexCloseLatch = new CountDownLatch(1);
444+
final AtomicReference<Throwable> cleanupException = new AtomicReference<>();
445+
when(cleanupExecutor.submit(any(Runnable.class))).thenAnswer(inv -> {
446+
final Runnable runnable = (Runnable) inv.getArguments()[0];
447+
return threads.submit(() -> {
448+
try {
449+
cleanupReadyLatch.countDown();
450+
assertTrue("index close did not completed in expected time", indexCloseLatch.await(1, TimeUnit.SECONDS));
451+
runnable.run();
452+
} catch (Throwable e) {
453+
logger.warn("caught error in cleanup thread", e);
454+
cleanupException.compareAndSet(null, e);
455+
} finally {
456+
cleanupCompleteLatch.countDown();
457+
}
458+
return null;
459+
});
460+
});
461+
462+
final DocumentSubsetBitsetCache cache = new DocumentSubsetBitsetCache(settings, cleanupExecutor);
463+
assertThat(cache.entryCount(), equalTo(0));
464+
assertThat(cache.ramBytesUsed(), equalTo(0L));
465+
466+
try {
467+
runTestOnIndex((searchExecutionContext, leafContext) -> {
468+
final Query query1 = QueryBuilders.termQuery("field-1", "value-1").toQuery(searchExecutionContext);
469+
final BitSet bitSet1 = cache.getBitSet(query1, leafContext);
470+
assertThat(bitSet1, notNullValue());
471+
472+
// Second query should trigger a cache eviction
473+
final Query query2 = QueryBuilders.termQuery("field-2", "value-2").toQuery(searchExecutionContext);
474+
final BitSet bitSet2 = cache.getBitSet(query2, leafContext);
475+
assertThat(bitSet2, notNullValue());
476+
477+
final IndexReader.CacheKey indexKey = leafContext.reader().getCoreCacheHelper().getKey();
478+
assertTrue("cleanup did not trigger in expected time", cleanupReadyLatch.await(1, TimeUnit.SECONDS));
479+
cache.onClose(indexKey);
480+
indexCloseLatch.countDown();
481+
assertTrue("cleanup did not complete in expected time", cleanupCompleteLatch.await(1, TimeUnit.SECONDS));
482+
assertThat("caught error in cleanup thread: " + cleanupException.get(), cleanupException.get(), nullValue());
483+
});
484+
} finally {
485+
threads.shutdown();
486+
}
487+
}
488+
449489
public void testCacheIsPerIndex() throws Exception {
450490
final DocumentSubsetBitsetCache cache = newCache(Settings.EMPTY);
451491
assertThat(cache.entryCount(), equalTo(0));

0 commit comments

Comments
 (0)