Skip to content

Commit 48a8c7e

Browse files
Ensure search contexts are removed on index delete (#56335) (#56617)
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 6de6ec6 commit 48a8c7e

File tree

3 files changed

+63
-1
lines changed

3 files changed

+63
-1
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
@@ -634,6 +634,9 @@ final SearchContext createAndPutContext(ShardSearchRequest request, SearchShardT
634634
boolean success = false;
635635
try {
636636
putContext(context);
637+
// ensure that if we race against afterIndexRemoved, we free the context here.
638+
// this is important to ensure store can be cleaned up, in particular if the search is a scroll with a long timeout.
639+
indicesService.indexServiceSafe(request.shardId().getIndex());
637640
success = true;
638641
return context;
639642
} finally {

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.elasticsearch.common.xcontent.XContentBuilder;
4747
import org.elasticsearch.index.Index;
4848
import org.elasticsearch.index.IndexModule;
49+
import org.elasticsearch.index.IndexNotFoundException;
4950
import org.elasticsearch.index.IndexService;
5051
import org.elasticsearch.index.IndexSettings;
5152
import org.elasticsearch.index.query.AbstractQueryBuilder;
@@ -122,7 +123,7 @@ protected boolean resetNodeAfterTest() {
122123
@Override
123124
protected Collection<Class<? extends Plugin>> getPlugins() {
124125
return pluginList(FailOnRewriteQueryPlugin.class, CustomScriptPlugin.class,
125-
ReaderWrapperCountPlugin.class, InternalOrPrivateSettingsPlugin.class);
126+
ReaderWrapperCountPlugin.class, InternalOrPrivateSettingsPlugin.class, MockSearchService.TestPlugin.class);
126127
}
127128

128129
public static class ReaderWrapperCountPlugin extends Plugin {
@@ -329,6 +330,7 @@ public void onFailure(Exception e) {
329330
service.executeFetchPhase(req, new SearchShardTask(123L, "", "", "", null, Collections.emptyMap()), listener);
330331
listener.get();
331332
if (useScroll) {
333+
// have to free context since this test does not remove the index from IndicesService.
332334
service.freeContext(searchPhaseResult.getContextId());
333335
}
334336
} catch (ExecutionException ex) {
@@ -357,6 +359,53 @@ public void onFailure(Exception e) {
357359
assertEquals(0, totalStats.getFetchCurrent());
358360
}
359361

362+
public void testSearchWhileIndexDeletedDoesNotLeakSearchContext() throws ExecutionException, InterruptedException {
363+
createIndex("index");
364+
client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get();
365+
366+
IndicesService indicesService = getInstanceFromNode(IndicesService.class);
367+
IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
368+
IndexShard indexShard = indexService.getShard(0);
369+
370+
MockSearchService service = (MockSearchService) getInstanceFromNode(SearchService.class);
371+
service.setOnPutContext(
372+
context -> {
373+
if (context.indexShard() == indexShard) {
374+
assertAcked(client().admin().indices().prepareDelete("index"));
375+
}
376+
}
377+
);
378+
379+
SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(true);
380+
SearchRequest scrollSearchRequest = new SearchRequest().allowPartialSearchResults(true)
381+
.scroll(new Scroll(TimeValue.timeValueMinutes(1)));
382+
383+
// the scrolls are not explicitly freed, but should all be gone when the test finished.
384+
// for completeness, we also randomly test the regular search path.
385+
final boolean useScroll = randomBoolean();
386+
PlainActionFuture<SearchPhaseResult> result = new PlainActionFuture<>();
387+
service.executeQueryPhase(
388+
new ShardSearchRequest(OriginalIndices.NONE, useScroll ? scrollSearchRequest : searchRequest,
389+
new ShardId(resolveIndex("index"), 0), 1,
390+
new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f, -1, null, null),
391+
new SearchShardTask(123L, "", "", "", null, Collections.emptyMap()), result);
392+
393+
try {
394+
result.get();
395+
} catch (Exception e) {
396+
// ok
397+
}
398+
399+
expectThrows(IndexNotFoundException.class, () -> client().admin().indices().prepareGetIndex().setIndices("index").get());
400+
401+
assertEquals(0, service.getActiveContexts());
402+
403+
SearchStats.Stats totalStats = indexShard.searchStats().getTotal();
404+
assertEquals(0, totalStats.getQueryCurrent());
405+
assertEquals(0, totalStats.getScrollCurrent());
406+
assertEquals(0, totalStats.getFetchCurrent());
407+
}
408+
360409
public void testTimeout() throws IOException {
361410
createIndex("index");
362411
final SearchService service = getInstanceFromNode(SearchService.class);
@@ -527,6 +576,8 @@ public void testMaxOpenScrollContexts() throws RuntimeException {
527576
SearchService.MAX_OPEN_SCROLL_CONTEXT.get(Settings.EMPTY) + "]. " +
528577
"This limit can be set by changing the [search.max_open_scroll_context] setting.",
529578
ex.getMessage());
579+
580+
service.freeAllScrollContexts();
530581
}
531582

532583
public void testOpenScrollContextsConcurrently() throws Exception {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.HashMap;
3434
import java.util.Map;
3535
import java.util.concurrent.ConcurrentHashMap;
36+
import java.util.function.Consumer;
3637

3738
public class MockSearchService extends SearchService {
3839
/**
@@ -42,6 +43,8 @@ public static class TestPlugin extends Plugin {}
4243

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

46+
private Consumer<SearchContext> onPutContext = context -> {};
47+
4548
/** Throw an {@link AssertionError} if there are still in-flight contexts. */
4649
public static void assertNoInFlightContext() {
4750
final Map<SearchContext, Throwable> copy = new HashMap<>(ACTIVE_SEARCH_CONTEXTS);
@@ -75,6 +78,7 @@ public MockSearchService(ClusterService clusterService,
7578

7679
@Override
7780
protected void putContext(SearchContext context) {
81+
onPutContext.accept(context);
7882
addActiveContext(context);
7983
super.putContext(context);
8084
}
@@ -87,4 +91,8 @@ protected SearchContext removeContext(long id) {
8791
}
8892
return removed;
8993
}
94+
95+
public void setOnPutContext(Consumer<SearchContext> onPutContext) {
96+
this.onPutContext = onPutContext;
97+
}
9098
}

0 commit comments

Comments
 (0)