Add SearchRestCancellationIT
This test verifies automatic cancellation of search requests on connection close. It was previously not present in 7.x as the http client was subject do a bug which made testing cancellation of requests impossible. Now that the bug is fixed upstream, we can also backport this test
This commit is contained in:
parent
030d43a76a
commit
de47ea2cf4
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.elasticsearch.http;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.lucene.util.SetOnce;
|
||||
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
|
||||
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
|
||||
import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchAction;
|
||||
import org.elasticsearch.action.support.WriteRequest;
|
||||
import org.elasticsearch.client.Cancellable;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.client.ResponseListener;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.plugins.PluginsService;
|
||||
import org.elasticsearch.script.MockScriptPlugin;
|
||||
import org.elasticsearch.script.Script;
|
||||
import org.elasticsearch.script.ScriptType;
|
||||
import org.elasticsearch.search.builder.SearchSourceBuilder;
|
||||
import org.elasticsearch.search.lookup.LeafFieldsLookup;
|
||||
import org.elasticsearch.tasks.CancellableTask;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.tasks.TaskId;
|
||||
import org.elasticsearch.tasks.TaskInfo;
|
||||
import org.elasticsearch.tasks.TaskManager;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.elasticsearch.index.query.QueryBuilders.scriptQuery;
|
||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
public class SearchRestCancellationIT extends HttpSmokeTestCase {
|
||||
|
||||
@Override
|
||||
protected Collection<Class<? extends Plugin>> nodePlugins() {
|
||||
List<Class<? extends Plugin>> plugins = new ArrayList<>();
|
||||
plugins.add(ScriptedBlockPlugin.class);
|
||||
plugins.addAll(super.nodePlugins());
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public void testAutomaticCancellationDuringQueryPhase() throws Exception {
|
||||
Map<String, String> nodeIdToName = readNodesInfo();
|
||||
|
||||
List<ScriptedBlockPlugin> plugins = initBlockFactory();
|
||||
indexTestData();
|
||||
|
||||
Request searchRequest = new Request("GET", "/test/_search");
|
||||
SearchSourceBuilder searchSource = new SearchSourceBuilder().query(scriptQuery(
|
||||
new Script(ScriptType.INLINE, "mockscript", ScriptedBlockPlugin.SCRIPT_NAME, Collections.emptyMap())));
|
||||
searchRequest.setJsonEntity(Strings.toString(searchSource));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Exception> error = new AtomicReference<>();
|
||||
Cancellable cancellable = getRestClient().performRequestAsync(searchRequest, new ResponseListener() {
|
||||
@Override
|
||||
public void onSuccess(Response response) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception exception) {
|
||||
error.set(exception);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
awaitForBlock(plugins);
|
||||
cancellable.cancel();
|
||||
ensureSearchTaskIsCancelled(nodeIdToName::get);
|
||||
|
||||
disableBlocks(plugins);
|
||||
latch.await();
|
||||
assertThat(error.get(), instanceOf(CancellationException.class));
|
||||
}
|
||||
|
||||
public void testAutomaticCancellationDuringFetchPhase() throws Exception {
|
||||
Map<String, String> nodeIdToName = readNodesInfo();
|
||||
|
||||
List<ScriptedBlockPlugin> plugins = initBlockFactory();
|
||||
indexTestData();
|
||||
|
||||
Request searchRequest = new Request("GET", "/test/_search");
|
||||
SearchSourceBuilder searchSource = new SearchSourceBuilder().scriptField("test_field",
|
||||
new Script(ScriptType.INLINE, "mockscript", ScriptedBlockPlugin.SCRIPT_NAME, Collections.emptyMap()));
|
||||
searchRequest.setJsonEntity(Strings.toString(searchSource));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Exception> error = new AtomicReference<>();
|
||||
Cancellable cancellable = getRestClient().performRequestAsync(searchRequest, new ResponseListener() {
|
||||
@Override
|
||||
public void onSuccess(Response response) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception exception) {
|
||||
error.set(exception);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
awaitForBlock(plugins);
|
||||
cancellable.cancel();
|
||||
ensureSearchTaskIsCancelled(nodeIdToName::get);
|
||||
|
||||
disableBlocks(plugins);
|
||||
latch.await();
|
||||
assertThat(error.get(), instanceOf(CancellationException.class));
|
||||
}
|
||||
|
||||
private static Map<String, String> readNodesInfo() {
|
||||
Map<String, String> nodeIdToName = new HashMap<>();
|
||||
NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo().get();
|
||||
assertFalse(nodesInfoResponse.hasFailures());
|
||||
for (NodeInfo node : nodesInfoResponse.getNodes()) {
|
||||
nodeIdToName.put(node.getNode().getId(), node.getNode().getName());
|
||||
}
|
||||
return nodeIdToName;
|
||||
}
|
||||
|
||||
private static void ensureSearchTaskIsCancelled(Function<String, String> nodeIdToName) throws Exception {
|
||||
SetOnce<TaskInfo> searchTask = new SetOnce<>();
|
||||
ListTasksResponse listTasksResponse = client().admin().cluster().prepareListTasks().get();
|
||||
for (TaskInfo task : listTasksResponse.getTasks()) {
|
||||
if (task.getAction().equals(SearchAction.NAME)) {
|
||||
searchTask.set(task);
|
||||
}
|
||||
}
|
||||
assertNotNull(searchTask.get());
|
||||
TaskId taskId = searchTask.get().getTaskId();
|
||||
String nodeName = nodeIdToName.apply(taskId.getNodeId());
|
||||
assertBusy(() -> {
|
||||
TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager();
|
||||
Task task = taskManager.getTask(taskId.getId());
|
||||
assertThat(task, instanceOf(CancellableTask.class));
|
||||
assertTrue(((CancellableTask)task).isCancelled());
|
||||
});
|
||||
}
|
||||
|
||||
private static void indexTestData() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Make sure we have a few segments
|
||||
BulkRequestBuilder bulkRequestBuilder = client().prepareBulk().setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
|
||||
for (int j = 0; j < 20; j++) {
|
||||
bulkRequestBuilder.add(client().prepareIndex("test", "_doc", Integer.toString(i * 5 + j)).setSource("field", "value"));
|
||||
}
|
||||
assertNoFailures(bulkRequestBuilder.get());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ScriptedBlockPlugin> initBlockFactory() {
|
||||
List<ScriptedBlockPlugin> plugins = new ArrayList<>();
|
||||
for (PluginsService pluginsService : internalCluster().getDataNodeInstances(PluginsService.class)) {
|
||||
plugins.addAll(pluginsService.filterPlugins(ScriptedBlockPlugin.class));
|
||||
}
|
||||
for (ScriptedBlockPlugin plugin : plugins) {
|
||||
plugin.reset();
|
||||
plugin.enableBlock();
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
private void awaitForBlock(List<ScriptedBlockPlugin> plugins) throws Exception {
|
||||
int numberOfShards = getNumShards("test").numPrimaries;
|
||||
assertBusy(() -> {
|
||||
int numberOfBlockedPlugins = 0;
|
||||
for (ScriptedBlockPlugin plugin : plugins) {
|
||||
numberOfBlockedPlugins += plugin.hits.get();
|
||||
}
|
||||
logger.info("The plugin blocked on {} out of {} shards", numberOfBlockedPlugins, numberOfShards);
|
||||
assertThat(numberOfBlockedPlugins, greaterThan(0));
|
||||
}, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static void disableBlocks(List<ScriptedBlockPlugin> plugins) {
|
||||
for (ScriptedBlockPlugin plugin : plugins) {
|
||||
plugin.disableBlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScriptedBlockPlugin extends MockScriptPlugin {
|
||||
static final String SCRIPT_NAME = "search_block";
|
||||
|
||||
private final AtomicInteger hits = new AtomicInteger();
|
||||
|
||||
private final AtomicBoolean shouldBlock = new AtomicBoolean(true);
|
||||
|
||||
void reset() {
|
||||
hits.set(0);
|
||||
}
|
||||
|
||||
void disableBlock() {
|
||||
shouldBlock.set(false);
|
||||
}
|
||||
|
||||
void enableBlock() {
|
||||
shouldBlock.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
|
||||
return Collections.singletonMap(SCRIPT_NAME, params -> {
|
||||
LeafFieldsLookup fieldsLookup = (LeafFieldsLookup) params.get("_fields");
|
||||
LogManager.getLogger(SearchRestCancellationIT.class).info("Blocking on the document {}", fieldsLookup.get("_id"));
|
||||
hits.incrementAndGet();
|
||||
try {
|
||||
awaitBusy(() -> shouldBlock.get() == false);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue