|
@@ -0,0 +1,378 @@
|
|
|
+/*
|
|
|
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
|
+ * or more contributor license agreements. Licensed under the Elastic License
|
|
|
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
|
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
|
+ * Side Public License, v 1.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.elasticsearch.health.node.action;
|
|
|
+
|
|
|
+import org.elasticsearch.Version;
|
|
|
+import org.elasticsearch.action.ActionListener;
|
|
|
+import org.elasticsearch.action.ActionRequest;
|
|
|
+import org.elasticsearch.action.ActionRequestValidationException;
|
|
|
+import org.elasticsearch.action.ActionResponse;
|
|
|
+import org.elasticsearch.action.support.ActionFilters;
|
|
|
+import org.elasticsearch.action.support.ActionTestUtils;
|
|
|
+import org.elasticsearch.action.support.PlainActionFuture;
|
|
|
+import org.elasticsearch.action.support.ThreadedActionListener;
|
|
|
+import org.elasticsearch.action.support.replication.ClusterStateCreationUtils;
|
|
|
+import org.elasticsearch.cluster.ClusterState;
|
|
|
+import org.elasticsearch.cluster.node.DiscoveryNode;
|
|
|
+import org.elasticsearch.cluster.node.DiscoveryNodeRole;
|
|
|
+import org.elasticsearch.cluster.service.ClusterService;
|
|
|
+import org.elasticsearch.common.io.stream.StreamInput;
|
|
|
+import org.elasticsearch.common.io.stream.StreamOutput;
|
|
|
+import org.elasticsearch.common.settings.Settings;
|
|
|
+import org.elasticsearch.tasks.CancellableTask;
|
|
|
+import org.elasticsearch.tasks.Task;
|
|
|
+import org.elasticsearch.tasks.TaskCancelledException;
|
|
|
+import org.elasticsearch.tasks.TaskId;
|
|
|
+import org.elasticsearch.tasks.TaskManager;
|
|
|
+import org.elasticsearch.test.ESTestCase;
|
|
|
+import org.elasticsearch.test.transport.CapturingTransport;
|
|
|
+import org.elasticsearch.threadpool.TestThreadPool;
|
|
|
+import org.elasticsearch.threadpool.ThreadPool;
|
|
|
+import org.elasticsearch.transport.TransportService;
|
|
|
+import org.junit.After;
|
|
|
+import org.junit.AfterClass;
|
|
|
+import org.junit.Before;
|
|
|
+import org.junit.BeforeClass;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
+import java.util.concurrent.ExecutionException;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+import static org.elasticsearch.test.ClusterServiceUtils.createClusterService;
|
|
|
+import static org.elasticsearch.test.ClusterServiceUtils.setState;
|
|
|
+import static org.hamcrest.Matchers.equalTo;
|
|
|
+import static org.hamcrest.Matchers.instanceOf;
|
|
|
+
|
|
|
+public class TransportHealthNodeActionTests extends ESTestCase {
|
|
|
+ private static ThreadPool threadPool;
|
|
|
+
|
|
|
+ private ClusterService clusterService;
|
|
|
+ private TransportService transportService;
|
|
|
+ private CapturingTransport transport;
|
|
|
+ private DiscoveryNode localNode;
|
|
|
+ private DiscoveryNode remoteNode;
|
|
|
+ private DiscoveryNode[] allNodes;
|
|
|
+ private TaskManager taskManager;
|
|
|
+
|
|
|
+ @BeforeClass
|
|
|
+ public static void beforeClass() {
|
|
|
+ threadPool = new TestThreadPool("TransportHealthNodeActionTests");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Before
|
|
|
+ @Override
|
|
|
+ public void setUp() throws Exception {
|
|
|
+ super.setUp();
|
|
|
+ taskManager = new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet());
|
|
|
+ transport = new CapturingTransport();
|
|
|
+ clusterService = createClusterService(threadPool);
|
|
|
+ transportService = transport.createTransportService(
|
|
|
+ clusterService.getSettings(),
|
|
|
+ threadPool,
|
|
|
+ TransportService.NOOP_TRANSPORT_INTERCEPTOR,
|
|
|
+ x -> clusterService.localNode(),
|
|
|
+ null,
|
|
|
+ Collections.emptySet()
|
|
|
+ );
|
|
|
+ transportService.start();
|
|
|
+ transportService.acceptIncomingRequests();
|
|
|
+ localNode = new DiscoveryNode(
|
|
|
+ "local_node",
|
|
|
+ buildNewFakeTransportAddress(),
|
|
|
+ Collections.emptyMap(),
|
|
|
+ Set.of(DiscoveryNodeRole.MASTER_ROLE, DiscoveryNodeRole.DATA_ROLE),
|
|
|
+ Version.CURRENT
|
|
|
+ );
|
|
|
+ remoteNode = new DiscoveryNode(
|
|
|
+ "remote_node",
|
|
|
+ buildNewFakeTransportAddress(),
|
|
|
+ Collections.emptyMap(),
|
|
|
+ Set.of(DiscoveryNodeRole.MASTER_ROLE, DiscoveryNodeRole.DATA_ROLE),
|
|
|
+ Version.CURRENT
|
|
|
+ );
|
|
|
+ allNodes = new DiscoveryNode[] { localNode, remoteNode };
|
|
|
+ }
|
|
|
+
|
|
|
+ @After
|
|
|
+ public void tearDown() throws Exception {
|
|
|
+ super.tearDown();
|
|
|
+ clusterService.close();
|
|
|
+ transportService.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterClass
|
|
|
+ public static void afterClass() {
|
|
|
+ ThreadPool.terminate(threadPool, 30, TimeUnit.SECONDS);
|
|
|
+ threadPool = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class Request extends ActionRequest {
|
|
|
+
|
|
|
+ Request() {}
|
|
|
+
|
|
|
+ Request(StreamInput in) throws IOException {
|
|
|
+ super(in);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ActionRequestValidationException validate() {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
|
|
|
+ return new CancellableTask(id, type, action, "", parentTaskId, headers);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static class Response extends ActionResponse {
|
|
|
+ private long identity = randomLong();
|
|
|
+
|
|
|
+ Response() {}
|
|
|
+
|
|
|
+ Response(StreamInput in) throws IOException {
|
|
|
+ super(in);
|
|
|
+ identity = in.readLong();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean equals(Object o) {
|
|
|
+ if (this == o) return true;
|
|
|
+ if (o == null || getClass() != o.getClass()) return false;
|
|
|
+ Response response = (Response) o;
|
|
|
+ return identity == response.identity;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int hashCode() {
|
|
|
+ return Objects.hash(identity);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void writeTo(StreamOutput out) throws IOException {
|
|
|
+ out.writeLong(identity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ class Action extends TransportHealthNodeAction<Request, Response> {
|
|
|
+ Action(String actionName, TransportService transportService, ClusterService clusterService, ThreadPool threadPool) {
|
|
|
+ this(actionName, transportService, clusterService, threadPool, ThreadPool.Names.SAME);
|
|
|
+ }
|
|
|
+
|
|
|
+ Action(
|
|
|
+ String actionName,
|
|
|
+ TransportService transportService,
|
|
|
+ ClusterService clusterService,
|
|
|
+ ThreadPool threadPool,
|
|
|
+ String executor
|
|
|
+ ) {
|
|
|
+ super(
|
|
|
+ actionName,
|
|
|
+ transportService,
|
|
|
+ clusterService,
|
|
|
+ threadPool,
|
|
|
+ new ActionFilters(new HashSet<>()),
|
|
|
+ Request::new,
|
|
|
+ Response::new,
|
|
|
+ executor
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doExecute(Task task, final Request request, ActionListener<Response> listener) {
|
|
|
+ // remove unneeded threading by wrapping listener with SAME to prevent super.doExecute from wrapping it with LISTENER
|
|
|
+ super.doExecute(task, request, new ThreadedActionListener<>(logger, threadPool, ThreadPool.Names.SAME, listener, false));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void healthOperation(Task task, Request request, ClusterState state, ActionListener<Response> listener) {
|
|
|
+ listener.onResponse(new Response());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ class WaitForSignalAction extends Action {
|
|
|
+ private final CountDownLatch countDownLatch;
|
|
|
+
|
|
|
+ WaitForSignalAction(
|
|
|
+ String actionName,
|
|
|
+ TransportService transportService,
|
|
|
+ ClusterService clusterService,
|
|
|
+ ThreadPool threadPool,
|
|
|
+ CountDownLatch countDownLatch
|
|
|
+ ) {
|
|
|
+ super(actionName, transportService, clusterService, threadPool, ThreadPool.Names.SAME);
|
|
|
+ this.countDownLatch = countDownLatch;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doExecute(Task task, final Request request, ActionListener<Response> listener) {
|
|
|
+ try {
|
|
|
+ countDownLatch.await();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ fail("Something went wrong while waiting for the latch");
|
|
|
+ }
|
|
|
+ super.doExecute(task, request, listener);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ class HealthOperationWithExceptionAction extends Action {
|
|
|
+
|
|
|
+ HealthOperationWithExceptionAction(
|
|
|
+ String actionName,
|
|
|
+ TransportService transportService,
|
|
|
+ ClusterService clusterService,
|
|
|
+ ThreadPool threadPool
|
|
|
+ ) {
|
|
|
+ super(actionName, transportService, clusterService, threadPool);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void healthOperation(Task task, Request request, ClusterState state, ActionListener<Response> listener) {
|
|
|
+ throw new RuntimeException("Simulated");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testLocalHealthNode() throws ExecutionException, InterruptedException {
|
|
|
+ final boolean healthOperationFailure = randomBoolean();
|
|
|
+
|
|
|
+ Request request = new Request();
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+
|
|
|
+ final Exception exception = new Exception();
|
|
|
+ final Response response = new Response();
|
|
|
+
|
|
|
+ setState(clusterService, ClusterStateCreationUtils.state(localNode, localNode, localNode, allNodes));
|
|
|
+
|
|
|
+ ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool) {
|
|
|
+ @Override
|
|
|
+ protected void healthOperation(Task task, Request request, ClusterState state, ActionListener<Response> listener) {
|
|
|
+ if (healthOperationFailure) {
|
|
|
+ listener.onFailure(exception);
|
|
|
+ } else {
|
|
|
+ listener.onResponse(response);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, null, request, listener);
|
|
|
+ assertTrue(listener.isDone());
|
|
|
+
|
|
|
+ if (healthOperationFailure) {
|
|
|
+ try {
|
|
|
+ listener.get();
|
|
|
+ fail("Expected exception but returned proper result");
|
|
|
+ } catch (ExecutionException ex) {
|
|
|
+ assertThat(ex.getCause(), equalTo(exception));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ assertThat(listener.get(), equalTo(response));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testHealthNodeNotAvailable() throws InterruptedException {
|
|
|
+ Request request = new Request();
|
|
|
+ setState(clusterService, ClusterStateCreationUtils.state(localNode, null, allNodes));
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+ ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool), null, request, listener);
|
|
|
+ assertTrue(listener.isDone());
|
|
|
+ try {
|
|
|
+ listener.get();
|
|
|
+ fail("NoHealthNodeSelectedException should be thrown");
|
|
|
+ } catch (ExecutionException ex) {
|
|
|
+ assertThat(ex.getCause(), instanceOf(HealthNodeNotDiscoveredException.class));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testDelegateToHealthNodeWithoutParentTask() throws ExecutionException, InterruptedException {
|
|
|
+ Request request = new Request();
|
|
|
+ setState(clusterService, ClusterStateCreationUtils.state(localNode, remoteNode, remoteNode, allNodes));
|
|
|
+
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+ ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool), null, request, listener);
|
|
|
+
|
|
|
+ assertThat(transport.capturedRequests().length, equalTo(1));
|
|
|
+ CapturingTransport.CapturedRequest capturedRequest = transport.capturedRequests()[0];
|
|
|
+ assertThat(capturedRequest.node(), equalTo(remoteNode));
|
|
|
+ assertThat(capturedRequest.request(), equalTo(request));
|
|
|
+ assertThat(capturedRequest.action(), equalTo("internal:testAction"));
|
|
|
+
|
|
|
+ Response response = new Response();
|
|
|
+ transport.handleResponse(capturedRequest.requestId(), response);
|
|
|
+ assertTrue(listener.isDone());
|
|
|
+ assertThat(listener.get(), equalTo(response));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testDelegateToHealthNodeWithParentTask() throws ExecutionException, InterruptedException {
|
|
|
+ Request request = new Request();
|
|
|
+ setState(clusterService, ClusterStateCreationUtils.state(localNode, remoteNode, remoteNode, allNodes));
|
|
|
+
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+ final CancellableTask task = (CancellableTask) taskManager.register("type", "internal:testAction", request);
|
|
|
+ ActionTestUtils.execute(new Action("internal:testAction", transportService, clusterService, threadPool), task, request, listener);
|
|
|
+
|
|
|
+ assertThat(transport.capturedRequests().length, equalTo(1));
|
|
|
+ CapturingTransport.CapturedRequest capturedRequest = transport.capturedRequests()[0];
|
|
|
+ assertThat(capturedRequest.node(), equalTo(remoteNode));
|
|
|
+ assertThat(capturedRequest.request(), equalTo(request));
|
|
|
+ assertThat(capturedRequest.action(), equalTo("internal:testAction"));
|
|
|
+
|
|
|
+ Response response = new Response();
|
|
|
+ transport.handleResponse(capturedRequest.requestId(), response);
|
|
|
+ assertTrue(listener.isDone());
|
|
|
+ assertThat(listener.get(), equalTo(response));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testHealthNodeOperationWithException() throws InterruptedException {
|
|
|
+ Request request = new Request();
|
|
|
+ setState(clusterService, ClusterStateCreationUtils.state(localNode, localNode, localNode, allNodes));
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+ ActionTestUtils.execute(
|
|
|
+ new HealthOperationWithExceptionAction("internal:testAction", transportService, clusterService, threadPool),
|
|
|
+ null,
|
|
|
+ request,
|
|
|
+ listener
|
|
|
+ );
|
|
|
+ assertTrue(listener.isDone());
|
|
|
+ try {
|
|
|
+ listener.get();
|
|
|
+ fail("A simulated RuntimeException should be thrown");
|
|
|
+ } catch (ExecutionException ex) {
|
|
|
+ assertThat(ex.getCause().getMessage(), equalTo("Simulated"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testTaskCancellation() {
|
|
|
+ Request request = new Request();
|
|
|
+ final CancellableTask task = (CancellableTask) taskManager.register("type", "internal:testAction", request);
|
|
|
+
|
|
|
+ PlainActionFuture<Response> listener = new PlainActionFuture<>();
|
|
|
+ CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
|
+
|
|
|
+ threadPool.executor(ThreadPool.Names.MANAGEMENT)
|
|
|
+ .submit(
|
|
|
+ () -> ActionTestUtils.execute(
|
|
|
+ new WaitForSignalAction("internal:testAction", transportService, clusterService, threadPool, countDownLatch),
|
|
|
+ task,
|
|
|
+ request,
|
|
|
+ listener
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ taskManager.cancel(task, "", () -> {});
|
|
|
+ assertThat(task.isCancelled(), equalTo(true));
|
|
|
+
|
|
|
+ countDownLatch.countDown();
|
|
|
+
|
|
|
+ expectThrows(TaskCancelledException.class, listener::actionGet);
|
|
|
+ }
|
|
|
+}
|