diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 804340d63ed11..2b287a4ad9790 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -365,6 +365,7 @@ public void apply(Settings value, Settings current, Settings previous) { SearchService.MAX_KEEPALIVE_SETTING, MultiBucketConsumerService.MAX_BUCKET_SETTING, SearchService.LOW_LEVEL_CANCELLATION_SETTING, + SearchService.MAX_OPEN_CONTEXT, Node.WRITE_PORTS_FILE_SETTING, Node.NODE_NAME_SETTING, Node.NODE_DATA_SETTING, diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index 047e7f47e62ec..c84eea3437dc9 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -136,6 +136,14 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv Setting.boolSetting("search.default_allow_partial_results", true, Property.Dynamic, Property.NodeScope); + /** + * A setting describing the maximum number of search contexts that can be created. The default + * maximum of 100 is defensive to prevent generating too many search contexts. + */ + public static final Setting MAX_OPEN_CONTEXT = + Setting.intSetting("search.max_open_context", 100, 0, Property.NodeScope); + + private final ThreadPool threadPool; private final ClusterService clusterService; @@ -170,6 +178,8 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv private final ConcurrentMapLong activeContexts = ConcurrentCollections.newConcurrentMapLongWithAggressiveConcurrency(); + private int numActiveContexts = 0; + private final MultiBucketConsumerService multiBucketConsumerService; public SearchService(ClusterService clusterService, IndicesService indicesService, @@ -547,6 +557,13 @@ private SearchContext findContext(long id, TransportRequest request) throws Sear } final SearchContext createAndPutContext(ShardSearchRequest request) throws IOException { + if (numActiveContexts >= MAX_OPEN_CONTEXT.get(settings)) { + throw new ElasticsearchException( + "Trying to create too many search contexts. Must be less than or equal to: [" + + MAX_OPEN_CONTEXT.get(settings) + "]. This limit can be set by changing the [" + + MAX_OPEN_CONTEXT.getKey() + "] setting."); + } + SearchContext context = createContext(request); boolean success = false; try { @@ -556,6 +573,7 @@ final SearchContext createAndPutContext(ShardSearchRequest request) throws IOExc } context.indexShard().getSearchOperationListener().onNewContext(context); success = true; + numActiveContexts++; return context; } finally { if (!success) { @@ -566,6 +584,7 @@ final SearchContext createAndPutContext(ShardSearchRequest request) throws IOExc final SearchContext createContext(ShardSearchRequest request) throws IOException { final DefaultSearchContext context = createSearchContext(request, defaultSearchTimeout); + try { if (request.scroll() != null) { context.scrollContext(new ScrollContext()); @@ -657,6 +676,7 @@ public boolean freeContext(long id) { } finally { context.close(); } + numActiveContexts--; return true; } return false; diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index f5552ee0d2e46..2a48d0577628b 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.store.AlreadyClosedException; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchPhaseExecutionException; @@ -343,6 +344,42 @@ searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_A } } + + /** + * test that creating more than the allowed number of search contexts throws an exception + */ + public void testMaxOpenContexts() throws IOException { + createIndex("index"); + final SearchService service = getInstanceFromNode(SearchService.class); + final IndicesService indicesService = getInstanceFromNode(IndicesService.class); + final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index")); + final IndexShard indexShard = indexService.getShard(0); + + for (int i = 0; i < service.MAX_OPEN_CONTEXT.get(Settings.EMPTY); i++) { + SearchContext context = service.createAndPutContext( + new ShardSearchLocalRequest( + indexShard.shardId(), + 1, + SearchType.DEFAULT, + new SearchSourceBuilder(), + new String[0], + false, + new AliasFilter(null, Strings.EMPTY_ARRAY), + 1.0f, true) + ); + } + + ElasticsearchException ex = expectThrows(ElasticsearchException.class, + () -> service.createAndPutContext(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.DEFAULT, + new SearchSourceBuilder(), new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1.0f, true))); + assertEquals( + "Trying to create too many search contexts. Must be less than or equal to: [" + + service.MAX_OPEN_CONTEXT.get(Settings.EMPTY) + "]. " + + "This limit can be set by changing the [search.max_open_context] setting.", + ex.getMessage()); + } + + public static class FailOnRewriteQueryPlugin extends Plugin implements SearchPlugin { @Override public List> getQueries() {