|
7 | 7 |
|
8 | 8 | package org.elasticsearch.xpack.profiler; |
9 | 9 |
|
| 10 | +import org.apache.http.entity.ContentType; |
| 11 | +import org.apache.http.entity.StringEntity; |
| 12 | +import org.apache.logging.log4j.LogManager; |
| 13 | +import org.apache.lucene.util.SetOnce; |
| 14 | +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; |
| 15 | +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; |
| 16 | +import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; |
10 | 17 | import org.elasticsearch.action.index.IndexResponse; |
| 18 | +import org.elasticsearch.action.support.PlainActionFuture; |
| 19 | +import org.elasticsearch.client.Cancellable; |
| 20 | +import org.elasticsearch.client.Request; |
| 21 | +import org.elasticsearch.client.Response; |
| 22 | +import org.elasticsearch.common.network.NetworkModule; |
11 | 23 | import org.elasticsearch.common.settings.Settings; |
12 | 24 | import org.elasticsearch.plugins.Plugin; |
| 25 | +import org.elasticsearch.plugins.PluginsService; |
13 | 26 | import org.elasticsearch.rest.RestStatus; |
| 27 | +import org.elasticsearch.script.MockScriptPlugin; |
| 28 | +import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; |
| 29 | +import org.elasticsearch.tasks.CancellableTask; |
| 30 | +import org.elasticsearch.tasks.Task; |
| 31 | +import org.elasticsearch.tasks.TaskId; |
| 32 | +import org.elasticsearch.tasks.TaskInfo; |
| 33 | +import org.elasticsearch.tasks.TaskManager; |
14 | 34 | import org.elasticsearch.test.ESIntegTestCase; |
| 35 | +import org.elasticsearch.transport.TransportService; |
| 36 | +import org.elasticsearch.transport.netty4.Netty4Plugin; |
15 | 37 | import org.elasticsearch.xcontent.XContentType; |
16 | 38 | import org.junit.Before; |
17 | 39 |
|
18 | 40 | import java.io.IOException; |
| 41 | +import java.nio.charset.StandardCharsets; |
| 42 | +import java.util.ArrayList; |
19 | 43 | import java.util.Collection; |
| 44 | +import java.util.Collections; |
| 45 | +import java.util.HashMap; |
20 | 46 | import java.util.List; |
21 | 47 | import java.util.Map; |
| 48 | +import java.util.concurrent.CancellationException; |
| 49 | +import java.util.concurrent.TimeUnit; |
| 50 | +import java.util.concurrent.atomic.AtomicBoolean; |
| 51 | +import java.util.concurrent.atomic.AtomicInteger; |
| 52 | +import java.util.function.Function; |
| 53 | + |
| 54 | +import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener; |
| 55 | +import static org.hamcrest.Matchers.greaterThan; |
| 56 | +import static org.hamcrest.Matchers.instanceOf; |
22 | 57 |
|
23 | 58 | @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 1) |
24 | 59 | public class GetProfilingActionIT extends ESIntegTestCase { |
25 | 60 | @Override |
26 | 61 | protected Collection<Class<? extends Plugin>> nodePlugins() { |
27 | | - return List.of(ProfilingPlugin.class); |
| 62 | + return List.of(ProfilingPlugin.class, ScriptedBlockPlugin.class, getTestTransportPlugin()); |
28 | 63 | } |
29 | 64 |
|
30 | 65 | @Override |
31 | 66 | protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { |
32 | 67 | return Settings.builder() |
33 | 68 | .put(super.nodeSettings(nodeOrdinal, otherSettings)) |
34 | 69 | .put(ProfilingPlugin.PROFILING_ENABLED.getKey(), true) |
| 70 | + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) |
| 71 | + .put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) |
35 | 72 | .build(); |
36 | 73 | } |
37 | 74 |
|
| 75 | + @Override |
| 76 | + protected boolean addMockHttpTransport() { |
| 77 | + return false; // enable http |
| 78 | + } |
| 79 | + |
| 80 | + @Override |
| 81 | + protected boolean ignoreExternalCluster() { |
| 82 | + return true; |
| 83 | + } |
| 84 | + |
38 | 85 | private byte[] read(String resource) throws IOException { |
39 | 86 | return GetProfilingAction.class.getClassLoader().getResourceAsStream(resource).readAllBytes(); |
40 | 87 | } |
@@ -104,4 +151,137 @@ public void testGetProfilingDataUnfiltered() throws Exception { |
104 | 151 | assertNotNull(response.getExecutables()); |
105 | 152 | assertNotNull("libc.so.6", response.getExecutables().get("QCCDqjSg3bMK1C4YRK6Tiw")); |
106 | 153 | } |
| 154 | + |
| 155 | + public void testAutomaticCancellation() throws Exception { |
| 156 | + Request restRequest = new Request("POST", "/_profiling/stacktraces"); |
| 157 | + restRequest.setEntity(new StringEntity(""" |
| 158 | + { |
| 159 | + "sample_size": 10000, |
| 160 | + "query": { |
| 161 | + "bool": { |
| 162 | + "filter": [ |
| 163 | + { |
| 164 | + "script": { |
| 165 | + "script": { |
| 166 | + "lang": "mockscript", |
| 167 | + "source": "search_block", |
| 168 | + "params": {} |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + ] |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + """, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8))); |
| 177 | + verifyCancellation(GetProfilingAction.NAME, restRequest); |
| 178 | + } |
| 179 | + |
| 180 | + void verifyCancellation(String action, Request restRequest) throws Exception { |
| 181 | + Map<String, String> nodeIdToName = readNodesInfo(); |
| 182 | + List<ScriptedBlockPlugin> plugins = initBlockFactory(); |
| 183 | + |
| 184 | + PlainActionFuture<Response> future = PlainActionFuture.newFuture(); |
| 185 | + Cancellable cancellable = getRestClient().performRequestAsync(restRequest, wrapAsRestResponseListener(future)); |
| 186 | + |
| 187 | + awaitForBlock(plugins); |
| 188 | + cancellable.cancel(); |
| 189 | + ensureTaskIsCancelled(action, nodeIdToName::get); |
| 190 | + |
| 191 | + disableBlocks(plugins); |
| 192 | + expectThrows(CancellationException.class, future::actionGet); |
| 193 | + } |
| 194 | + |
| 195 | + private static Map<String, String> readNodesInfo() { |
| 196 | + Map<String, String> nodeIdToName = new HashMap<>(); |
| 197 | + NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo().get(); |
| 198 | + assertFalse(nodesInfoResponse.hasFailures()); |
| 199 | + for (NodeInfo node : nodesInfoResponse.getNodes()) { |
| 200 | + nodeIdToName.put(node.getNode().getId(), node.getNode().getName()); |
| 201 | + } |
| 202 | + return nodeIdToName; |
| 203 | + } |
| 204 | + |
| 205 | + private static void ensureTaskIsCancelled(String transportAction, Function<String, String> nodeIdToName) throws Exception { |
| 206 | + SetOnce<TaskInfo> searchTask = new SetOnce<>(); |
| 207 | + ListTasksResponse listTasksResponse = client().admin().cluster().prepareListTasks().get(); |
| 208 | + for (TaskInfo task : listTasksResponse.getTasks()) { |
| 209 | + if (task.action().equals(transportAction)) { |
| 210 | + searchTask.set(task); |
| 211 | + } |
| 212 | + } |
| 213 | + assertNotNull(searchTask.get()); |
| 214 | + TaskId taskId = searchTask.get().taskId(); |
| 215 | + String nodeName = nodeIdToName.apply(taskId.getNodeId()); |
| 216 | + assertBusy(() -> { |
| 217 | + TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager(); |
| 218 | + Task task = taskManager.getTask(taskId.getId()); |
| 219 | + assertThat(task, instanceOf(CancellableTask.class)); |
| 220 | + assertTrue(((CancellableTask) task).isCancelled()); |
| 221 | + }); |
| 222 | + } |
| 223 | + |
| 224 | + private static List<ScriptedBlockPlugin> initBlockFactory() { |
| 225 | + List<ScriptedBlockPlugin> plugins = new ArrayList<>(); |
| 226 | + for (PluginsService pluginsService : internalCluster().getDataNodeInstances(PluginsService.class)) { |
| 227 | + plugins.addAll(pluginsService.filterPlugins(ScriptedBlockPlugin.class)); |
| 228 | + } |
| 229 | + for (ScriptedBlockPlugin plugin : plugins) { |
| 230 | + plugin.reset(); |
| 231 | + plugin.enableBlock(); |
| 232 | + } |
| 233 | + return plugins; |
| 234 | + } |
| 235 | + |
| 236 | + private void awaitForBlock(List<ScriptedBlockPlugin> plugins) throws Exception { |
| 237 | + assertBusy(() -> { |
| 238 | + int numberOfBlockedPlugins = 0; |
| 239 | + for (ScriptedBlockPlugin plugin : plugins) { |
| 240 | + numberOfBlockedPlugins += plugin.hits.get(); |
| 241 | + } |
| 242 | + logger.info("The plugin blocked on {} shards", numberOfBlockedPlugins); |
| 243 | + assertThat(numberOfBlockedPlugins, greaterThan(0)); |
| 244 | + }, 10, TimeUnit.SECONDS); |
| 245 | + } |
| 246 | + |
| 247 | + private static void disableBlocks(List<ScriptedBlockPlugin> plugins) { |
| 248 | + for (ScriptedBlockPlugin plugin : plugins) { |
| 249 | + plugin.disableBlock(); |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + public static class ScriptedBlockPlugin extends MockScriptPlugin { |
| 254 | + static final String SCRIPT_NAME = "search_block"; |
| 255 | + |
| 256 | + private final AtomicInteger hits = new AtomicInteger(); |
| 257 | + |
| 258 | + private final AtomicBoolean shouldBlock = new AtomicBoolean(true); |
| 259 | + |
| 260 | + void reset() { |
| 261 | + hits.set(0); |
| 262 | + } |
| 263 | + |
| 264 | + void disableBlock() { |
| 265 | + shouldBlock.set(false); |
| 266 | + } |
| 267 | + |
| 268 | + void enableBlock() { |
| 269 | + shouldBlock.set(true); |
| 270 | + } |
| 271 | + |
| 272 | + @Override |
| 273 | + public Map<String, Function<Map<String, Object>, Object>> pluginScripts() { |
| 274 | + return Collections.singletonMap(SCRIPT_NAME, params -> { |
| 275 | + LeafStoredFieldsLookup fieldsLookup = (LeafStoredFieldsLookup) params.get("_fields"); |
| 276 | + LogManager.getLogger(GetProfilingActionIT.class).info("Blocking on the document {}", fieldsLookup.get("_id")); |
| 277 | + hits.incrementAndGet(); |
| 278 | + try { |
| 279 | + waitUntil(() -> shouldBlock.get() == false); |
| 280 | + } catch (Exception e) { |
| 281 | + throw new RuntimeException(e); |
| 282 | + } |
| 283 | + return true; |
| 284 | + }); |
| 285 | + } |
| 286 | + } |
107 | 287 | } |
0 commit comments