Skip to content

Commit eed50b4

Browse files
committed
Replace version with reader cache key in IndicesRequestCache (#34189)
Today we use the version of a DirectoryReader as a component of the key of IndicesRequestCache. This usage is perfectly fine since the version is advanced every time a new change is made into IndexWriter. In other words, two DirectoryReaders with the same version should have the same content. However, this invariant is only guaranteed in the context of a single IndexWriter because the version is reset to the committed version value when IndexWriter is re-opened. Since #33473, each IndexShard may have more than one IndexWriter, and using the version of a DirectoryReader as a part of the cache key can cause IndicesRequestCache to return stale cached values. For example, in #27650, we rollback the engine (i.e., re-open IndexWriter), index new documents, refresh, then make a count request, but the search layer mistakenly returns the count of the DirectoryReader of the previous IndexWriter because the current DirectoryReader has the same version of the old DirectoryReader even their documents are different. This is possible because these two readers come from different IndexWriters. This commit replaces the the version with the reader cache key of IndexReader as a component of the cache key of IndicesRequestCache. Closes #27650 Relates #33473
1 parent 8de6726 commit eed50b4

File tree

5 files changed

+80
-29
lines changed

5 files changed

+80
-29
lines changed

qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ protected void doRun() throws Exception {
111111
return future;
112112
}
113113

114-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/27650")
115114
public void testRecoveryWithConcurrentIndexing() throws Exception {
116115
final String index = "recovery_with_concurrent_indexing";
117116
Response response = client().performRequest(new Request("GET", "_nodes"));

server/src/main/java/org/elasticsearch/indices/IndicesRequestCache.java

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@
4444
import java.util.Collection;
4545
import java.util.Collections;
4646
import java.util.Iterator;
47+
import java.util.Objects;
4748
import java.util.Set;
4849
import java.util.concurrent.ConcurrentMap;
4950
import java.util.function.Supplier;
5051

5152
/**
5253
* The indices request cache allows to cache a shard level request stage responses, helping with improving
5354
* similar requests that are potentially expensive (because of aggs for example). The cache is fully coherent
54-
* with the semantics of NRT (the index reader version is part of the cache key), and relies on size based
55+
* with the semantics of NRT (the index reader cache key is part of the cache key), and relies on size based
5556
* eviction to evict old reader associated cache entries as well as scheduler reaper to clean readers that
5657
* are no longer used or closed shards.
5758
* <p>
@@ -100,7 +101,7 @@ public void close() {
100101
}
101102

102103
void clear(CacheEntity entity) {
103-
keysToClean.add(new CleanupKey(entity, -1));
104+
keysToClean.add(new CleanupKey(entity, null));
104105
cleanCache();
105106
}
106107

@@ -111,13 +112,14 @@ public void onRemoval(RemovalNotification<Key, BytesReference> notification) {
111112

112113
BytesReference getOrCompute(CacheEntity cacheEntity, Supplier<BytesReference> loader,
113114
DirectoryReader reader, BytesReference cacheKey) throws Exception {
114-
final Key key = new Key(cacheEntity, reader.getVersion(), cacheKey);
115+
assert reader.getReaderCacheHelper() != null;
116+
final Key key = new Key(cacheEntity, reader.getReaderCacheHelper().getKey(), cacheKey);
115117
Loader cacheLoader = new Loader(cacheEntity, loader);
116118
BytesReference value = cache.computeIfAbsent(key, cacheLoader);
117119
if (cacheLoader.isLoaded()) {
118120
key.entity.onMiss();
119121
// see if its the first time we see this reader, and make sure to register a cleanup key
120-
CleanupKey cleanupKey = new CleanupKey(cacheEntity, reader.getVersion());
122+
CleanupKey cleanupKey = new CleanupKey(cacheEntity, reader.getReaderCacheHelper().getKey());
121123
if (!registeredClosedListeners.containsKey(cleanupKey)) {
122124
Boolean previous = registeredClosedListeners.putIfAbsent(cleanupKey, Boolean.TRUE);
123125
if (previous == null) {
@@ -137,7 +139,8 @@ BytesReference getOrCompute(CacheEntity cacheEntity, Supplier<BytesReference> lo
137139
* @param cacheKey the cache key to invalidate
138140
*/
139141
void invalidate(CacheEntity cacheEntity, DirectoryReader reader, BytesReference cacheKey) {
140-
cache.invalidate(new Key(cacheEntity, reader.getVersion(), cacheKey));
142+
assert reader.getReaderCacheHelper() != null;
143+
cache.invalidate(new Key(cacheEntity, reader.getReaderCacheHelper().getKey(), cacheKey));
141144
}
142145

143146
private static class Loader implements CacheLoader<Key, BytesReference> {
@@ -206,12 +209,12 @@ static class Key implements Accountable {
206209
private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(Key.class);
207210

208211
public final CacheEntity entity; // use as identity equality
209-
public final long readerVersion; // use the reader version to now keep a reference to a "short" lived reader until its reaped
212+
public final IndexReader.CacheKey readerCacheKey;
210213
public final BytesReference value;
211214

212-
Key(CacheEntity entity, long readerVersion, BytesReference value) {
215+
Key(CacheEntity entity, IndexReader.CacheKey readerCacheKey, BytesReference value) {
213216
this.entity = entity;
214-
this.readerVersion = readerVersion;
217+
this.readerCacheKey = Objects.requireNonNull(readerCacheKey);
215218
this.value = value;
216219
}
217220

@@ -231,7 +234,7 @@ public boolean equals(Object o) {
231234
if (this == o) return true;
232235
if (o == null || getClass() != o.getClass()) return false;
233236
Key key = (Key) o;
234-
if (readerVersion != key.readerVersion) return false;
237+
if (Objects.equals(readerCacheKey, key.readerCacheKey) == false) return false;
235238
if (!entity.getCacheIdentity().equals(key.entity.getCacheIdentity())) return false;
236239
if (!value.equals(key.value)) return false;
237240
return true;
@@ -240,19 +243,19 @@ public boolean equals(Object o) {
240243
@Override
241244
public int hashCode() {
242245
int result = entity.getCacheIdentity().hashCode();
243-
result = 31 * result + Long.hashCode(readerVersion);
246+
result = 31 * result + readerCacheKey.hashCode();
244247
result = 31 * result + value.hashCode();
245248
return result;
246249
}
247250
}
248251

249252
private class CleanupKey implements IndexReader.ClosedListener {
250253
final CacheEntity entity;
251-
final long readerVersion; // use the reader version to now keep a reference to a "short" lived reader until its reaped
254+
final IndexReader.CacheKey readerCacheKey;
252255

253-
private CleanupKey(CacheEntity entity, long readerVersion) {
256+
private CleanupKey(CacheEntity entity, IndexReader.CacheKey readerCacheKey) {
254257
this.entity = entity;
255-
this.readerVersion = readerVersion;
258+
this.readerCacheKey = readerCacheKey;
256259
}
257260

258261
@Override
@@ -270,15 +273,15 @@ public boolean equals(Object o) {
270273
return false;
271274
}
272275
CleanupKey that = (CleanupKey) o;
273-
if (readerVersion != that.readerVersion) return false;
276+
if (Objects.equals(readerCacheKey, that.readerCacheKey) == false) return false;
274277
if (!entity.getCacheIdentity().equals(that.entity.getCacheIdentity())) return false;
275278
return true;
276279
}
277280

278281
@Override
279282
public int hashCode() {
280283
int result = entity.getCacheIdentity().hashCode();
281-
result = 31 * result + Long.hashCode(readerVersion);
284+
result = 31 * result + Objects.hashCode(readerCacheKey);
282285
return result;
283286
}
284287
}
@@ -293,7 +296,7 @@ synchronized void cleanCache() {
293296
for (Iterator<CleanupKey> iterator = keysToClean.iterator(); iterator.hasNext(); ) {
294297
CleanupKey cleanupKey = iterator.next();
295298
iterator.remove();
296-
if (cleanupKey.readerVersion == -1 || cleanupKey.entity.isOpen() == false) {
299+
if (cleanupKey.readerCacheKey == null || cleanupKey.entity.isOpen() == false) {
297300
// -1 indicates full cleanup, as does a closed shard
298301
currentFullClean.add(cleanupKey.entity.getCacheIdentity());
299302
} else {
@@ -306,7 +309,7 @@ synchronized void cleanCache() {
306309
if (currentFullClean.contains(key.entity.getCacheIdentity())) {
307310
iterator.remove();
308311
} else {
309-
if (currentKeysToClean.contains(new CleanupKey(key.entity, key.readerVersion))) {
312+
if (currentKeysToClean.contains(new CleanupKey(key.entity, key.readerCacheKey))) {
310313
iterator.remove();
311314
}
312315
}

server/src/main/java/org/elasticsearch/indices/IndicesService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,10 +1166,9 @@ public boolean canCache(ShardSearchRequest request, SearchContext context) {
11661166
} else if (request.requestCache() == false) {
11671167
return false;
11681168
}
1169-
// if the reader is not a directory reader, we can't get the version from it
1170-
if ((context.searcher().getIndexReader() instanceof DirectoryReader) == false) {
1171-
return false;
1172-
}
1169+
// We use the cacheKey of the index reader as a part of a key of the IndicesRequestCache.
1170+
assert context.searcher().getIndexReader().getReaderCacheHelper() != null;
1171+
11731172
// if now in millis is used (or in the future, a more generic "isDeterministic" flag
11741173
// then we can't cache based on "now" key within the search request, as it is not deterministic
11751174
if (context.getQueryShardContext().isCachable() == false) {

server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
2626
import org.elasticsearch.action.admin.indices.stats.IndexStats;
2727
import org.elasticsearch.action.index.IndexRequest;
28+
import org.elasticsearch.action.search.SearchRequest;
2829
import org.elasticsearch.action.search.SearchResponse;
2930
import org.elasticsearch.action.support.IndicesOptions;
3031
import org.elasticsearch.cluster.ClusterInfoService;
@@ -67,6 +68,7 @@
6768
import org.elasticsearch.indices.recovery.RecoveryState;
6869
import org.elasticsearch.plugins.Plugin;
6970
import org.elasticsearch.search.aggregations.AggregationBuilders;
71+
import org.elasticsearch.search.builder.SearchSourceBuilder;
7072
import org.elasticsearch.test.DummyShardLock;
7173
import org.elasticsearch.test.ESSingleNodeTestCase;
7274
import org.elasticsearch.test.IndexSettingsModule;
@@ -713,4 +715,37 @@ public void testGlobalCheckpointListenerTimeout() throws InterruptedException {
713715
assertTrue(notified.get());
714716
}
715717

718+
public void testInvalidateIndicesRequestCacheWhenRollbackEngine() throws Exception {
719+
createIndex("test", Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)
720+
.put("index.refresh_interval", -1).build());
721+
ensureGreen();
722+
final IndicesService indicesService = getInstanceFromNode(IndicesService.class);
723+
final IndexShard shard = indicesService.getShardOrNull(new ShardId(resolveIndex("test"), 0));
724+
final SearchRequest countRequest = new SearchRequest("test").source(new SearchSourceBuilder().size(0));
725+
final long numDocs = between(10, 20);
726+
for (int i = 0; i < numDocs; i++) {
727+
client().prepareIndex("test", "_doc", Integer.toString(i)).setSource("{}", XContentType.JSON).get();
728+
if (randomBoolean()) {
729+
shard.refresh("test");
730+
}
731+
}
732+
shard.refresh("test");
733+
assertThat(client().search(countRequest).actionGet().getHits().totalHits, equalTo(numDocs));
734+
assertThat(shard.getLocalCheckpoint(), equalTo(shard.seqNoStats().getMaxSeqNo()));
735+
shard.resetEngineToGlobalCheckpoint();
736+
final long moreDocs = between(10, 20);
737+
for (int i = 0; i < moreDocs; i++) {
738+
client().prepareIndex("test", "_doc", Long.toString(i + numDocs)).setSource("{}", XContentType.JSON).get();
739+
if (randomBoolean()) {
740+
shard.refresh("test");
741+
}
742+
}
743+
shard.refresh("test");
744+
try (Engine.Searcher searcher = shard.acquireSearcher("test")) {
745+
assertThat("numDocs=" + numDocs + " moreDocs=" + moreDocs, (long) searcher.reader().numDocs(), equalTo(numDocs + moreDocs));
746+
}
747+
assertThat("numDocs=" + numDocs + " moreDocs=" + moreDocs,
748+
client().search(countRequest).actionGet().getHits().totalHits, equalTo(numDocs + moreDocs));
749+
}
750+
716751
}

server/src/test/java/org/elasticsearch/indices/IndicesRequestCacheTests.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import org.apache.lucene.document.Field;
2424
import org.apache.lucene.document.StringField;
2525
import org.apache.lucene.index.DirectoryReader;
26+
import org.apache.lucene.index.IndexReader;
2627
import org.apache.lucene.index.IndexWriter;
28+
import org.apache.lucene.index.IndexWriterConfig;
2729
import org.apache.lucene.index.Term;
2830
import org.apache.lucene.search.IndexSearcher;
2931
import org.apache.lucene.search.TermQuery;
@@ -119,7 +121,11 @@ public void testCacheDifferentReaders() throws Exception {
119121
DirectoryReader reader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(writer), new ShardId("foo", "bar", 1));
120122
TermQueryBuilder termQuery = new TermQueryBuilder("id", "0");
121123
BytesReference termBytes = XContentHelper.toXContent(termQuery, XContentType.JSON, false);
122-
124+
if (randomBoolean()) {
125+
writer.flush();
126+
IOUtils.close(writer);
127+
writer = new IndexWriter(dir, newIndexWriterConfig());
128+
}
123129
writer.updateDocument(new Term("id", "0"), newDoc(0, "bar"));
124130
DirectoryReader secondReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(writer), new ShardId("foo", "bar", 1));
125131

@@ -424,14 +430,23 @@ public void testInvalidate() throws Exception {
424430
assertEquals(0, cache.numRegisteredCloseListeners());
425431
}
426432

427-
public void testEqualsKey() {
433+
public void testEqualsKey() throws IOException {
428434
AtomicBoolean trueBoolean = new AtomicBoolean(true);
429435
AtomicBoolean falseBoolean = new AtomicBoolean(false);
430-
IndicesRequestCache.Key key1 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), 1L, new TestBytesReference(1));
431-
IndicesRequestCache.Key key2 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), 1L, new TestBytesReference(1));
432-
IndicesRequestCache.Key key3 = new IndicesRequestCache.Key(new TestEntity(null, falseBoolean), 1L, new TestBytesReference(1));
433-
IndicesRequestCache.Key key4 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), 2L, new TestBytesReference(1));
434-
IndicesRequestCache.Key key5 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), 1L, new TestBytesReference(2));
436+
Directory dir = newDirectory();
437+
IndexWriterConfig config = newIndexWriterConfig();
438+
IndexWriter writer = new IndexWriter(dir, config);
439+
IndexReader reader1 = DirectoryReader.open(writer);
440+
IndexReader.CacheKey rKey1 = reader1.getReaderCacheHelper().getKey();
441+
writer.addDocument(new Document());
442+
IndexReader reader2 = DirectoryReader.open(writer);
443+
IndexReader.CacheKey rKey2 = reader2.getReaderCacheHelper().getKey();
444+
IOUtils.close(reader1, reader2, writer, dir);
445+
IndicesRequestCache.Key key1 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), rKey1, new TestBytesReference(1));
446+
IndicesRequestCache.Key key2 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), rKey1, new TestBytesReference(1));
447+
IndicesRequestCache.Key key3 = new IndicesRequestCache.Key(new TestEntity(null, falseBoolean), rKey1, new TestBytesReference(1));
448+
IndicesRequestCache.Key key4 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), rKey2, new TestBytesReference(1));
449+
IndicesRequestCache.Key key5 = new IndicesRequestCache.Key(new TestEntity(null, trueBoolean), rKey1, new TestBytesReference(2));
435450
String s = "Some other random object";
436451
assertEquals(key1, key1);
437452
assertEquals(key1, key2);

0 commit comments

Comments
 (0)