Skip to content

Commit 10c4ab6

Browse files
Ensure search contexts are removed on index delete (#56335) (#56617) (#56744)
In a race condition, a search context could remain enlisted in SearchService when an index is deleted, potentially causing the index folder to not be cleaned up (for either lengthy searches or scrolls with timeouts > 30 minutes or if the scroll is kept active).
1 parent dec7c21 commit 10c4ab6

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

server/src/main/java/org/elasticsearch/search/SearchService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,9 @@ final SearchContext createAndPutContext(ShardSearchRequest request) throws IOExc
607607
boolean success = false;
608608
try {
609609
putContext(context);
610+
// ensure that if we race against afterIndexRemoved, we free the context here.
611+
// this is important to ensure store can be cleaned up, in particular if the search is a scroll with a long timeout.
612+
indicesService.indexServiceSafe(request.shardId().getIndex());
610613
success = true;
611614
return context;
612615
} finally {

server/src/test/java/org/elasticsearch/search/SearchServiceTests.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.elasticsearch.common.xcontent.XContentBuilder;
4343
import org.elasticsearch.index.Index;
4444
import org.elasticsearch.index.IndexModule;
45+
import org.elasticsearch.index.IndexNotFoundException;
4546
import org.elasticsearch.index.IndexService;
4647
import org.elasticsearch.index.IndexSettings;
4748
import org.elasticsearch.index.query.AbstractQueryBuilder;
@@ -113,7 +114,8 @@ protected boolean resetNodeAfterTest() {
113114

114115
@Override
115116
protected Collection<Class<? extends Plugin>> getPlugins() {
116-
return pluginList(FailOnRewriteQueryPlugin.class, CustomScriptPlugin.class, InternalOrPrivateSettingsPlugin.class);
117+
return pluginList(FailOnRewriteQueryPlugin.class, CustomScriptPlugin.class, InternalOrPrivateSettingsPlugin.class,
118+
MockSearchService.TestPlugin.class);
117119
}
118120

119121
public static class CustomScriptPlugin extends MockScriptPlugin {
@@ -288,6 +290,7 @@ public void onFailure(Exception e) {
288290
service.executeFetchPhase(req, new SearchTask(123L, "", "", "", null, Collections.emptyMap()), listener);
289291
listener.get();
290292
if (useScroll) {
293+
// have to free context since this test does not remove the index from IndicesService.
291294
service.freeContext(searchPhaseResult.getRequestId());
292295
}
293296
} catch (ExecutionException ex) {
@@ -316,6 +319,59 @@ public void onFailure(Exception e) {
316319
assertEquals(0, totalStats.getFetchCurrent());
317320
}
318321

322+
public void testSearchWhileIndexDeletedDoesNotLeakSearchContext() throws ExecutionException, InterruptedException {
323+
createIndex("index");
324+
client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get();
325+
326+
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
327+
IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
328+
IndexShard indexShard = indexService.getShard(0);
329+
330+
MockSearchService service = (MockSearchService) getInstanceFromNode(SearchService.class);
331+
service.setOnPutContext(
332+
context -> {
333+
if (context.indexShard() == indexShard) {
334+
assertAcked(client().admin().indices().prepareDelete("index"));
335+
}
336+
}
337+
);
338+
339+
SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(true);
340+
SearchRequest scrollSearchRequest = new SearchRequest().allowPartialSearchResults(true)
341+
.scroll(new Scroll(TimeValue.timeValueMinutes(1)));
342+
343+
// the scrolls are not explicitly freed, but should all be gone when the test finished.
344+
// for completeness, we also randomly test the regular search path.
345+
final boolean useScroll = randomBoolean();
346+
PlainActionFuture<SearchPhaseResult> result = new PlainActionFuture<>();
347+
ShardSearchLocalRequest shardRequest;
348+
if (useScroll) {
349+
shardRequest = new ShardScrollRequestTest(indexShard.shardId());
350+
} else {
351+
shardRequest = new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT,
352+
new SearchSourceBuilder(), new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f,
353+
true, null, null);
354+
}
355+
service.executeQueryPhase(
356+
shardRequest,
357+
new SearchTask(123L, "", "", "", null, Collections.emptyMap()), result);
358+
359+
try {
360+
result.get();
361+
} catch (Exception e) {
362+
// ok
363+
}
364+
365+
expectThrows(IndexNotFoundException.class, () -> client().admin().indices().prepareGetIndex().setIndices("index").get());
366+
367+
assertEquals(0, service.getActiveContexts());
368+
369+
SearchStats.Stats totalStats = indexShard.searchStats().getTotal();
370+
assertEquals(0, totalStats.getQueryCurrent());
371+
assertEquals(0, totalStats.getScrollCurrent());
372+
assertEquals(0, totalStats.getFetchCurrent());
373+
}
374+
319375
public void testTimeout() throws IOException {
320376
createIndex("index");
321377
final SearchService service = getInstanceFromNode(SearchService.class);
@@ -480,6 +536,9 @@ public void testMaxOpenScrollContexts() throws RuntimeException, IOException {
480536
"This limit can be set by changing the [search.max_open_scroll_context] setting."
481537
)
482538
);
539+
clearScrollRequest = new ClearScrollRequest();
540+
clearScrollRequest.addScrollId("_all");
541+
client().clearScroll(clearScrollRequest);
483542
} finally {
484543
client().admin().cluster().prepareUpdateSettings()
485544
.setPersistentSettings(

test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.HashMap;
3333
import java.util.Map;
3434
import java.util.concurrent.ConcurrentHashMap;
35+
import java.util.function.Consumer;
3536

3637
public class MockSearchService extends SearchService {
3738
/**
@@ -41,6 +42,8 @@ public static class TestPlugin extends Plugin {}
4142

4243
private static final Map<SearchContext, Throwable> ACTIVE_SEARCH_CONTEXTS = new ConcurrentHashMap<>();
4344

45+
private Consumer<SearchContext> onPutContext = context -> {};
46+
4447
/** Throw an {@link AssertionError} if there are still in-flight contexts. */
4548
public static void assertNoInFlightContext() {
4649
final Map<SearchContext, Throwable> copy = new HashMap<>(ACTIVE_SEARCH_CONTEXTS);
@@ -74,8 +77,9 @@ public MockSearchService(ClusterService clusterService,
7477

7578
@Override
7679
protected void putContext(SearchContext context) {
77-
super.putContext(context);
80+
onPutContext.accept(context);
7881
addActiveContext(context);
82+
super.putContext(context);
7983
}
8084

8185
@Override
@@ -86,4 +90,8 @@ protected SearchContext removeContext(long id) {
8690
}
8791
return removed;
8892
}
93+
94+
public void setOnPutContext(Consumer<SearchContext> onPutContext) {
95+
this.onPutContext = onPutContext;
96+
}
8997
}

0 commit comments

Comments
 (0)