Skip to content

Commit a475323

Browse files
authored
Invalidate cached query results if query timed out (#22807)
Today we cache query results even if the query timed out. This is obviously problematic since results are not complete. Yet, the decision if a query timed out or not happens too late to simply not cache the result since if we'd just throw an exception all currently waiting requests with the same request / cache key would fail with the same exception without the option to access the result or to re-execute. Instead, this change will allow the request to enter the cache but invalidates it immediately. Concurrent request might not get executed and return the timed out result which is not absolutely correct but very likely since identical requests will likely timeout as well. As a side-effect we won't hammer the node with concurrent slow searches but rather only execute one of them and return shortly cached result. Closes #22789
1 parent a383bc1 commit a475323

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ BytesReference getOrCompute(CacheEntity cacheEntity, Supplier<BytesReference> lo
130130
return value;
131131
}
132132

133+
/**
134+
* Invalidates the given the cache entry for the given key and it's context
135+
* @param cacheEntity the cache entity to invalidate for
136+
* @param reader the reader to invalidate the cache entry for
137+
* @param cacheKey the cache key to invalidate
138+
*/
139+
void invalidate(CacheEntity cacheEntity, DirectoryReader reader, BytesReference cacheKey) {
140+
cache.invalidate(new Key(cacheEntity, reader.getVersion(), cacheKey));
141+
}
142+
133143
private static class Loader implements CacheLoader<Key, BytesReference> {
134144

135145
private final CacheEntity entity;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,17 +1134,28 @@ public void loadIntoContext(ShardSearchRequest request, SearchContext context, Q
11341134
queryPhase.execute(context);
11351135
try {
11361136
context.queryResult().writeToNoId(out);
1137+
11371138
} catch (IOException e) {
11381139
throw new AssertionError("Could not serialize response", e);
11391140
}
11401141
loadedFromCache[0] = false;
11411142
});
1143+
11421144
if (loadedFromCache[0]) {
11431145
// restore the cached query result into the context
11441146
final QuerySearchResult result = context.queryResult();
11451147
StreamInput in = new NamedWriteableAwareStreamInput(bytesReference.streamInput(), namedWriteableRegistry);
11461148
result.readFromWithId(context.id(), in);
11471149
result.shardTarget(context.shardTarget());
1150+
} else if (context.queryResult().searchTimedOut()) {
1151+
// we have to invalidate the cache entry if we cached a query result form a request that timed out.
1152+
// we can't really throw exceptions in the loading part to signal a timed out search to the outside world since if there are
1153+
// multiple requests that wait for the cache entry to be calculated they'd fail all with the same exception.
1154+
// instead we all caching such a result for the time being, return the timed out result for all other searches with that cache
1155+
// key invalidate the result in the thread that caused the timeout. This will end up to be simpler and eventually correct since
1156+
// running a search that times out concurrently will likely timeout again if it's run while we have this `stale` result in the
1157+
// cache. One other option is to not cache requests with a timeout at all...
1158+
indicesRequestCache.invalidate(new IndexShardCacheEntity(context.indexShard()), directoryReader, request.cacheKey());
11481159
}
11491160
}
11501161

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

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ public void testCacheDifferentReaders() throws Exception {
169169
assertEquals(2, requestCacheStats.stats().getMissCount());
170170
assertEquals(0, requestCacheStats.stats().getEvictions());
171171
assertTrue(loader.loadedFromCache);
172-
assertTrue(loader.loadedFromCache);
173172
assertEquals(1, cache.count());
174173
assertEquals(cacheSize, requestCacheStats.stats().getMemorySize().bytesAsInt());
175174
assertEquals(1, cache.numRegisteredCloseListeners());
@@ -186,7 +185,6 @@ public void testCacheDifferentReaders() throws Exception {
186185
assertEquals(2, requestCacheStats.stats().getMissCount());
187186
assertEquals(0, requestCacheStats.stats().getEvictions());
188187
assertTrue(loader.loadedFromCache);
189-
assertTrue(loader.loadedFromCache);
190188
assertEquals(0, cache.count());
191189
assertEquals(0, requestCacheStats.stats().getMemorySize().bytesAsInt());
192190

@@ -215,7 +213,7 @@ public void testEviction() throws Exception {
215213
new ShardId("foo", "bar", 1));
216214
TestEntity secondEntity = new TestEntity(requestCacheStats, indexShard);
217215
Loader secondLoader = new Loader(secondReader, 0);
218-
216+
219217
BytesReference value1 = cache.getOrCompute(entity, loader, reader, termQuery.buildAsBytes());
220218
assertEquals("foo", value1.streamInput().readString());
221219
BytesReference value2 = cache.getOrCompute(secondEntity, secondLoader, secondReader, termQuery.buildAsBytes());
@@ -347,6 +345,74 @@ public BytesReference get() {
347345

348346
}
349347

348+
public void testInvalidate() throws Exception {
349+
ShardRequestCache requestCacheStats = new ShardRequestCache();
350+
IndicesRequestCache cache = new IndicesRequestCache(Settings.EMPTY);
351+
Directory dir = newDirectory();
352+
IndexWriter writer = new IndexWriter(dir, newIndexWriterConfig());
353+
354+
writer.addDocument(newDoc(0, "foo"));
355+
DirectoryReader reader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(writer),
356+
new ShardId("foo", "bar", 1));
357+
TermQueryBuilder termQuery = new TermQueryBuilder("id", "0");
358+
AtomicBoolean indexShard = new AtomicBoolean(true);
359+
360+
// initial cache
361+
TestEntity entity = new TestEntity(requestCacheStats, indexShard);
362+
Loader loader = new Loader(reader, 0);
363+
BytesReference value = cache.getOrCompute(entity, loader, reader, termQuery.buildAsBytes());
364+
assertEquals("foo", value.streamInput().readString());
365+
assertEquals(0, requestCacheStats.stats().getHitCount());
366+
assertEquals(1, requestCacheStats.stats().getMissCount());
367+
assertEquals(0, requestCacheStats.stats().getEvictions());
368+
assertFalse(loader.loadedFromCache);
369+
assertEquals(1, cache.count());
370+
371+
// cache hit
372+
entity = new TestEntity(requestCacheStats, indexShard);
373+
loader = new Loader(reader, 0);
374+
value = cache.getOrCompute(entity, loader, reader, termQuery.buildAsBytes());
375+
assertEquals("foo", value.streamInput().readString());
376+
assertEquals(1, requestCacheStats.stats().getHitCount());
377+
assertEquals(1, requestCacheStats.stats().getMissCount());
378+
assertEquals(0, requestCacheStats.stats().getEvictions());
379+
assertTrue(loader.loadedFromCache);
380+
assertEquals(1, cache.count());
381+
assertTrue(requestCacheStats.stats().getMemorySize().bytesAsInt() > value.length());
382+
assertEquals(1, cache.numRegisteredCloseListeners());
383+
384+
// load again after invalidate
385+
entity = new TestEntity(requestCacheStats, indexShard);
386+
loader = new Loader(reader, 0);
387+
cache.invalidate(entity, reader, termQuery.buildAsBytes());
388+
value = cache.getOrCompute(entity, loader, reader, termQuery.buildAsBytes());
389+
assertEquals("foo", value.streamInput().readString());
390+
assertEquals(1, requestCacheStats.stats().getHitCount());
391+
assertEquals(2, requestCacheStats.stats().getMissCount());
392+
assertEquals(0, requestCacheStats.stats().getEvictions());
393+
assertFalse(loader.loadedFromCache);
394+
assertEquals(1, cache.count());
395+
assertTrue(requestCacheStats.stats().getMemorySize().bytesAsInt() > value.length());
396+
assertEquals(1, cache.numRegisteredCloseListeners());
397+
398+
// release
399+
if (randomBoolean()) {
400+
reader.close();
401+
} else {
402+
indexShard.set(false); // closed shard but reader is still open
403+
cache.clear(entity);
404+
}
405+
cache.cleanCache();
406+
assertEquals(1, requestCacheStats.stats().getHitCount());
407+
assertEquals(2, requestCacheStats.stats().getMissCount());
408+
assertEquals(0, requestCacheStats.stats().getEvictions());
409+
assertEquals(0, cache.count());
410+
assertEquals(0, requestCacheStats.stats().getMemorySize().bytesAsInt());
411+
412+
IOUtils.close(reader, writer, dir, cache);
413+
assertEquals(0, cache.numRegisteredCloseListeners());
414+
}
415+
350416
private class TestEntity extends AbstractIndexShardCacheEntity {
351417
private final AtomicBoolean standInForIndexShard;
352418
private final ShardRequestCache shardRequestCache;

0 commit comments

Comments
 (0)