|
14 | 14 | import org.elasticsearch.action.ActionRequestValidationException; |
15 | 15 | import org.elasticsearch.action.ActionResponse; |
16 | 16 | import org.elasticsearch.action.support.ActionFilters; |
| 17 | +import org.elasticsearch.action.support.ActionTestUtils; |
17 | 18 | import org.elasticsearch.action.support.PlainActionFuture; |
18 | 19 | import org.elasticsearch.action.support.ThreadedActionListener; |
19 | 20 | import org.elasticsearch.action.support.replication.ClusterStateCreationUtils; |
|
30 | 31 | import org.elasticsearch.cluster.service.ClusterService; |
31 | 32 | import org.elasticsearch.common.io.stream.StreamInput; |
32 | 33 | import org.elasticsearch.common.io.stream.StreamOutput; |
| 34 | +import org.elasticsearch.common.settings.Settings; |
33 | 35 | import org.elasticsearch.common.unit.TimeValue; |
| 36 | +import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor; |
34 | 37 | import org.elasticsearch.discovery.MasterNotDiscoveredException; |
35 | 38 | import org.elasticsearch.indices.TestIndexNameExpressionResolver; |
36 | 39 | import org.elasticsearch.node.NodeClosedException; |
37 | 40 | import org.elasticsearch.rest.RestStatus; |
| 41 | +import org.elasticsearch.tasks.CancellableTask; |
38 | 42 | import org.elasticsearch.tasks.Task; |
| 43 | +import org.elasticsearch.tasks.TaskId; |
| 44 | +import org.elasticsearch.tasks.TaskManager; |
39 | 45 | import org.elasticsearch.test.ESTestCase; |
40 | 46 | import org.elasticsearch.test.transport.CapturingTransport; |
41 | 47 | import org.elasticsearch.threadpool.TestThreadPool; |
|
50 | 56 | import java.io.IOException; |
51 | 57 | import java.util.Collections; |
52 | 58 | import java.util.HashSet; |
| 59 | +import java.util.Map; |
53 | 60 | import java.util.Objects; |
54 | 61 | import java.util.Set; |
| 62 | +import java.util.concurrent.CancellationException; |
| 63 | +import java.util.concurrent.CountDownLatch; |
| 64 | +import java.util.concurrent.CyclicBarrier; |
55 | 65 | import java.util.concurrent.ExecutionException; |
56 | 66 | import java.util.concurrent.TimeUnit; |
57 | 67 |
|
@@ -125,6 +135,11 @@ public static class Request extends MasterNodeRequest<Request> { |
125 | 135 | public ActionRequestValidationException validate() { |
126 | 136 | return null; |
127 | 137 | } |
| 138 | + |
| 139 | + @Override |
| 140 | + public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) { |
| 141 | + return new CancellableTask(id, type, action, "", parentTaskId, headers); |
| 142 | + } |
128 | 143 | } |
129 | 144 |
|
130 | 145 | class Response extends ActionResponse { |
@@ -159,12 +174,18 @@ public void writeTo(StreamOutput out) throws IOException { |
159 | 174 | class Action extends TransportMasterNodeAction<Request, Response> { |
160 | 175 | Action(String actionName, TransportService transportService, ClusterService clusterService, |
161 | 176 | ThreadPool threadPool) { |
| 177 | + this(actionName, transportService, clusterService, threadPool, ThreadPool.Names.SAME); |
| 178 | + } |
| 179 | + |
| 180 | + Action(String actionName, TransportService transportService, ClusterService clusterService, |
| 181 | + ThreadPool threadPool, String executor) { |
162 | 182 | super(actionName, transportService, clusterService, threadPool, |
163 | 183 | new ActionFilters(new HashSet<>()), Request::new, |
164 | 184 | TestIndexNameExpressionResolver.newInstance(), Response::new, |
165 | | - ThreadPool.Names.SAME); |
| 185 | + executor); |
166 | 186 | } |
167 | 187 |
|
| 188 | + |
168 | 189 | @Override |
169 | 190 | protected void doExecute(Task task, final Request request, ActionListener<Response> listener) { |
170 | 191 | // remove unneeded threading by wrapping listener with SAME to prevent super.doExecute from wrapping it with LISTENER |
@@ -458,4 +479,110 @@ protected void masterOperation(Request request, ClusterState state, ActionListen |
458 | 479 | assertTrue(listener.isDone()); |
459 | 480 | assertThat(listener.get(), equalTo(response)); |
460 | 481 | } |
| 482 | + |
| 483 | + public void testTaskCancellation() { |
| 484 | + ClusterBlock block = new ClusterBlock(1, |
| 485 | + "", |
| 486 | + true, |
| 487 | + true, |
| 488 | + false, |
| 489 | + randomFrom(RestStatus.values()), |
| 490 | + ClusterBlockLevel.ALL |
| 491 | + ); |
| 492 | + ClusterState stateWithBlock = ClusterState.builder(ClusterStateCreationUtils.state(localNode, localNode, allNodes)) |
| 493 | + .blocks(ClusterBlocks.builder().addGlobalBlock(block)).build(); |
| 494 | + |
| 495 | + // Update the cluster state with a block so the request waits until it's unblocked |
| 496 | + setState(clusterService, stateWithBlock); |
| 497 | + |
| 498 | + TaskManager taskManager = new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet()); |
| 499 | + |
| 500 | + Request request = new Request(); |
| 501 | + final CancellableTask task = (CancellableTask) taskManager.register("type", "internal:testAction", request); |
| 502 | + |
| 503 | + boolean cancelBeforeStart = randomBoolean(); |
| 504 | + if (cancelBeforeStart) { |
| 505 | + taskManager.cancel(task, "", () -> {}); |
| 506 | + assertThat(task.isCancelled(), equalTo(true)); |
| 507 | + } |
| 508 | + |
| 509 | + PlainActionFuture<Response> listener = new PlainActionFuture<>(); |
| 510 | + ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool) { |
| 511 | + @Override |
| 512 | + protected ClusterBlockException checkBlock(Request request, ClusterState state) { |
| 513 | + Set<ClusterBlock> blocks = state.blocks().global(); |
| 514 | + return blocks.isEmpty() ? null : new ClusterBlockException(blocks); |
| 515 | + } |
| 516 | + }, task, request, listener); |
| 517 | + |
| 518 | + final int genericThreads = threadPool.info(ThreadPool.Names.GENERIC).getMax(); |
| 519 | + final EsThreadPoolExecutor executor = (EsThreadPoolExecutor) threadPool.executor(ThreadPool.Names.GENERIC); |
| 520 | + final CyclicBarrier barrier = new CyclicBarrier(genericThreads + 1); |
| 521 | + final CountDownLatch latch = new CountDownLatch(1); |
| 522 | + |
| 523 | + if (cancelBeforeStart == false) { |
| 524 | + assertThat(listener.isDone(), equalTo(false)); |
| 525 | + |
| 526 | + taskManager.cancel(task, "", () -> {}); |
| 527 | + assertThat(task.isCancelled(), equalTo(true)); |
| 528 | + |
| 529 | + // Now that the task is cancelled, let the request to be executed |
| 530 | + final ClusterState.Builder newStateBuilder = ClusterState.builder(stateWithBlock); |
| 531 | + |
| 532 | + // Either unblock the cluster state or just do an unrelated cluster state change that will check |
| 533 | + // if the task has been cancelled |
| 534 | + if (randomBoolean()) { |
| 535 | + newStateBuilder.blocks(ClusterBlocks.EMPTY_CLUSTER_BLOCK); |
| 536 | + } else { |
| 537 | + newStateBuilder.incrementVersion(); |
| 538 | + } |
| 539 | + setState(clusterService, newStateBuilder.build()); |
| 540 | + } |
| 541 | + expectThrows(CancellationException.class, listener::actionGet); |
| 542 | + } |
| 543 | + |
| 544 | + public void testTaskCancellationOnceActionItIsDispatchedToMaster() throws Exception { |
| 545 | + TaskManager taskManager = new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet()); |
| 546 | + |
| 547 | + Request request = new Request(); |
| 548 | + final CancellableTask task = (CancellableTask) taskManager.register("type", "internal:testAction", request); |
| 549 | + |
| 550 | + // Block all the threads of the executor in which the master operation will be dispatched to |
| 551 | + // ensure that the master operation won't be executed until the threads are released |
| 552 | + final String executorName = ThreadPool.Names.GENERIC; |
| 553 | + final Runnable releaseBlockedThreads = blockAllThreads(executorName); |
| 554 | + |
| 555 | + PlainActionFuture<Response> listener = new PlainActionFuture<>(); |
| 556 | + ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool, executorName), |
| 557 | + task, |
| 558 | + request, |
| 559 | + listener |
| 560 | + ); |
| 561 | + |
| 562 | + taskManager.cancel(task, "", () -> {}); |
| 563 | + assertThat(task.isCancelled(), equalTo(true)); |
| 564 | + |
| 565 | + releaseBlockedThreads.run(); |
| 566 | + |
| 567 | + expectThrows(CancellationException.class, listener::actionGet); |
| 568 | + } |
| 569 | + |
| 570 | + private Runnable blockAllThreads(String executorName) throws Exception { |
| 571 | + final int numberOfThreads = threadPool.info(executorName).getMax(); |
| 572 | + final EsThreadPoolExecutor executor = (EsThreadPoolExecutor) threadPool.executor(executorName); |
| 573 | + final CyclicBarrier barrier = new CyclicBarrier(numberOfThreads + 1); |
| 574 | + final CountDownLatch latch = new CountDownLatch(1); |
| 575 | + for (int i = 0; i < numberOfThreads; i++) { |
| 576 | + executor.submit(() -> { |
| 577 | + try { |
| 578 | + barrier.await(); |
| 579 | + latch.await(); |
| 580 | + } catch (Exception e) { |
| 581 | + throw new AssertionError(e); |
| 582 | + } |
| 583 | + }); |
| 584 | + } |
| 585 | + barrier.await(); |
| 586 | + return latch::countDown; |
| 587 | + } |
461 | 588 | } |
0 commit comments