diff --git a/docs/changelog/87984.yaml b/docs/changelog/87984.yaml new file mode 100644 index 0000000000000..8f3a3e5f028eb --- /dev/null +++ b/docs/changelog/87984.yaml @@ -0,0 +1,5 @@ +pr: 87984 +summary: Creating a transport action for the `CoordinationDiagnosticsService` +area: Health +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 753ac8124e062..1858d8ab28784 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -17,6 +17,7 @@ import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction; import org.elasticsearch.action.admin.cluster.configuration.TransportClearVotingConfigExclusionsAction; import org.elasticsearch.action.admin.cluster.coordination.ClusterFormationInfoAction; +import org.elasticsearch.action.admin.cluster.coordination.CoordinationDiagnosticsAction; import org.elasticsearch.action.admin.cluster.coordination.MasterHistoryAction; import org.elasticsearch.action.admin.cluster.desirednodes.DeleteDesiredNodesAction; import org.elasticsearch.action.admin.cluster.desirednodes.GetDesiredNodesAction; @@ -639,6 +640,7 @@ public void reg actions.register(AnalyzeIndexDiskUsageAction.INSTANCE, TransportAnalyzeIndexDiskUsageAction.class); actions.register(FieldUsageStatsAction.INSTANCE, TransportFieldUsageAction.class); actions.register(MasterHistoryAction.INSTANCE, MasterHistoryAction.TransportAction.class); + actions.register(CoordinationDiagnosticsAction.INSTANCE, CoordinationDiagnosticsAction.TransportAction.class); // Indexed scripts actions.register(PutStoredScriptAction.INSTANCE, TransportPutStoredScriptAction.class); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsAction.java new file mode 100644 index 0000000000000..913003c446d5a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsAction.java @@ -0,0 +1,138 @@ +/* + * 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.action.admin.cluster.coordination; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsResult; + +/** + * This action exposes CoordinationDiagnosticsService#diagnoseMasterStability so that a node can get a remote node's view of + * coordination diagnostics (including master stability). + */ +public class CoordinationDiagnosticsAction extends ActionType { + + public static final CoordinationDiagnosticsAction INSTANCE = new CoordinationDiagnosticsAction(); + public static final String NAME = "cluster:internal/coordination_diagnostics/info"; + + private CoordinationDiagnosticsAction() { + super(NAME, CoordinationDiagnosticsAction.Response::new); + } + + public static class Request extends ActionRequest { + final boolean explain; // Non-private for testing + + public Request(boolean explain) { + this.explain = explain; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Request(StreamInput in) throws IOException { + super(in); + this.explain = in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(explain); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return explain == ((Request) o).explain; + } + + @Override + public int hashCode() { + return Objects.hashCode(explain); + } + + } + + public static class Response extends ActionResponse { + + private final CoordinationDiagnosticsResult result; + + public Response(StreamInput in) throws IOException { + super(in); + result = new CoordinationDiagnosticsResult(in); + } + + public Response(CoordinationDiagnosticsResult result) { + this.result = result; + } + + public CoordinationDiagnosticsResult getCoordinationDiagnosticsResult() { + return result; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + result.writeTo(out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CoordinationDiagnosticsAction.Response response = (CoordinationDiagnosticsAction.Response) o; + return result.equals(response.result); + } + + @Override + public int hashCode() { + return Objects.hash(result); + } + } + + /** + * This transport action calls CoordinationDiagnosticsService#diagnoseMasterStability + */ + public static class TransportAction extends HandledTransportAction { + private final CoordinationDiagnosticsService coordinationDiagnosticsService; + + @Inject + public TransportAction( + TransportService transportService, + ActionFilters actionFilters, + CoordinationDiagnosticsService coordinationDiagnosticsService + ) { + super(CoordinationDiagnosticsAction.NAME, transportService, actionFilters, CoordinationDiagnosticsAction.Request::new); + this.coordinationDiagnosticsService = coordinationDiagnosticsService; + } + + @Override + protected void doExecute(Task task, CoordinationDiagnosticsAction.Request request, ActionListener listener) { + listener.onResponse(new Response(coordinationDiagnosticsService.diagnoseMasterStability(request.explain))); + } + } + +} diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsService.java b/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsService.java index 2f65b430d2f10..f7716a64a9189 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsService.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsService.java @@ -14,10 +14,14 @@ import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collection; @@ -412,13 +416,34 @@ public record CoordinationDiagnosticsResult( CoordinationDiagnosticsStatus status, String summary, CoordinationDiagnosticsDetails details - ) {} + ) implements Writeable { - public enum CoordinationDiagnosticsStatus { + public CoordinationDiagnosticsResult(StreamInput in) throws IOException { + this(CoordinationDiagnosticsStatus.fromStreamInput(in), in.readString(), new CoordinationDiagnosticsDetails(in)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + status.writeTo(out); + out.writeString(summary); + details.writeTo(out); + } + } + + public enum CoordinationDiagnosticsStatus implements Writeable { GREEN, UNKNOWN, YELLOW, RED; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(this); + } + + public static CoordinationDiagnosticsStatus fromStreamInput(StreamInput in) throws IOException { + return in.readEnum(CoordinationDiagnosticsStatus.class); + } } public record CoordinationDiagnosticsDetails( @@ -427,7 +452,7 @@ public record CoordinationDiagnosticsDetails( @Nullable String remoteExceptionMessage, @Nullable String remoteExceptionStackTrace, @Nullable String clusterFormationDescription - ) { + ) implements Writeable { public CoordinationDiagnosticsDetails(DiscoveryNode currentMaster, List recentMasters) { this(currentMaster, recentMasters, null, null, null); @@ -437,6 +462,32 @@ public CoordinationDiagnosticsDetails(DiscoveryNode currentMaster, Exception rem this(currentMaster, null, remoteException == null ? null : remoteException.getMessage(), getStackTrace(remoteException), null); } + public CoordinationDiagnosticsDetails(StreamInput in) throws IOException { + this(readCurrentMaster(in), readRecentMasters(in), in.readOptionalString(), in.readOptionalString(), in.readOptionalString()); + } + + private static DiscoveryNode readCurrentMaster(StreamInput in) throws IOException { + boolean hasCurrentMaster = in.readBoolean(); + DiscoveryNode currentMaster; + if (hasCurrentMaster) { + currentMaster = new DiscoveryNode(in); + } else { + currentMaster = null; + } + return currentMaster; + } + + private static List readRecentMasters(StreamInput in) throws IOException { + boolean hasRecentMasters = in.readBoolean(); + List recentMasters; + if (hasRecentMasters) { + recentMasters = in.readImmutableList(DiscoveryNode::new); + } else { + recentMasters = null; + } + return recentMasters; + } + private static String getStackTrace(Exception e) { if (e == null) { return null; @@ -447,5 +498,25 @@ private static String getStackTrace(Exception e) { } public static final CoordinationDiagnosticsDetails EMPTY = new CoordinationDiagnosticsDetails(null, null, null, null, null); + + @Override + public void writeTo(StreamOutput out) throws IOException { + if (currentMaster == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + currentMaster.writeTo(out); + } + if (recentMasters == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeList(recentMasters); + } + out.writeOptionalString(remoteExceptionMessage); + out.writeOptionalString(remoteExceptionStackTrace); + out.writeOptionalString(clusterFormationDescription); + } + } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsActionTests.java new file mode 100644 index 0000000000000..cb6cb4a01cb0f --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/coordination/CoordinationDiagnosticsActionTests.java @@ -0,0 +1,81 @@ +/* + * 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.action.admin.cluster.coordination; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodeRole; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsDetails; +import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsResult; +import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsStatus; + +public class CoordinationDiagnosticsActionTests extends ESTestCase { + + public void testSerialization() { + DiscoveryNode node1 = new DiscoveryNode( + "node1", + UUID.randomUUID().toString(), + buildNewFakeTransportAddress(), + Collections.emptyMap(), + DiscoveryNodeRole.roles(), + Version.CURRENT + ); + DiscoveryNode node2 = new DiscoveryNode( + "node2", + UUID.randomUUID().toString(), + buildNewFakeTransportAddress(), + Collections.emptyMap(), + DiscoveryNodeRole.roles(), + Version.CURRENT + ); + CoordinationDiagnosticsDetails details = new CoordinationDiagnosticsDetails( + node1, + List.of(node1, node2), + randomAlphaOfLengthBetween(0, 30), + randomAlphaOfLengthBetween(0, 30), + randomAlphaOfLengthBetween(0, 30) + ); + CoordinationDiagnosticsResult result = new CoordinationDiagnosticsResult( + randomFrom(CoordinationDiagnosticsStatus.values()), + randomAlphaOfLength(100), + details + ); + CoordinationDiagnosticsAction.Response response = new CoordinationDiagnosticsAction.Response(result); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + response, + history -> copyWriteable(history, writableRegistry(), CoordinationDiagnosticsAction.Response::new), + this::mutateResponse + ); + + CoordinationDiagnosticsAction.Request request = new CoordinationDiagnosticsAction.Request(randomBoolean()); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + request, + history -> copyWriteable(history, writableRegistry(), CoordinationDiagnosticsAction.Request::new), + this::mutateRequest + ); + } + + private CoordinationDiagnosticsAction.Request mutateRequest(CoordinationDiagnosticsAction.Request originalRequest) { + return new CoordinationDiagnosticsAction.Request(originalRequest.explain == false); + } + + private CoordinationDiagnosticsAction.Response mutateResponse(CoordinationDiagnosticsAction.Response originalResponse) { + CoordinationDiagnosticsResult originalResult = originalResponse.getCoordinationDiagnosticsResult(); + return new CoordinationDiagnosticsAction.Response( + new CoordinationDiagnosticsResult(originalResult.status(), randomAlphaOfLength(100), originalResult.details()) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java index a430dc549e206..047b39151bf90 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java @@ -20,11 +20,13 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.monitor.StatusInfo; +import org.elasticsearch.test.EqualsHashCodeTestUtils; import org.elasticsearch.threadpool.ThreadPool; import org.junit.Before; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -584,6 +586,152 @@ public void testRedForNoMasterAndWithMasterEligibleNodesAndNoLeader() throws IOE } } + public void testResultSerialization() { + CoordinationDiagnosticsService.CoordinationDiagnosticsStatus status = getRandomStatus(); + CoordinationDiagnosticsService.CoordinationDiagnosticsDetails details = getRandomDetails(); + CoordinationDiagnosticsService.CoordinationDiagnosticsResult result = + new CoordinationDiagnosticsService.CoordinationDiagnosticsResult(status, randomAlphaOfLength(30), details); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + result, + history -> copyWriteable(result, writableRegistry(), CoordinationDiagnosticsService.CoordinationDiagnosticsResult::new), + this::mutateResult + ); + } + + public void testStatusSerialization() { + CoordinationDiagnosticsService.CoordinationDiagnosticsStatus status = getRandomStatus(); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + status, + history -> copyWriteable( + status, + writableRegistry(), + CoordinationDiagnosticsService.CoordinationDiagnosticsStatus::fromStreamInput + ), + this::mutateStatus + ); + } + + public void testDetailsSerialization() { + CoordinationDiagnosticsService.CoordinationDiagnosticsDetails details = getRandomDetails(); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + details, + history -> copyWriteable(details, writableRegistry(), CoordinationDiagnosticsService.CoordinationDiagnosticsDetails::new), + this::mutateDetails + ); + } + + private CoordinationDiagnosticsService.CoordinationDiagnosticsDetails mutateDetails( + CoordinationDiagnosticsService.CoordinationDiagnosticsDetails originalDetails + ) { + switch (randomIntBetween(1, 5)) { + case 1 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + node2, + originalDetails.recentMasters(), + originalDetails.remoteExceptionMessage(), + originalDetails.remoteExceptionStackTrace(), + originalDetails.clusterFormationDescription() + ); + } + case 2 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + originalDetails.currentMaster(), + List.of(node1, node2, node3), + originalDetails.remoteExceptionMessage(), + originalDetails.remoteExceptionStackTrace(), + originalDetails.clusterFormationDescription() + ); + } + case 3 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + originalDetails.currentMaster(), + originalDetails.recentMasters(), + randomAlphaOfLength(30), + originalDetails.remoteExceptionStackTrace(), + originalDetails.clusterFormationDescription() + ); + } + case 4 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + originalDetails.currentMaster(), + originalDetails.recentMasters(), + originalDetails.remoteExceptionMessage(), + randomAlphaOfLength(100), + originalDetails.clusterFormationDescription() + ); + } + case 5 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + originalDetails.currentMaster(), + originalDetails.recentMasters(), + originalDetails.remoteExceptionMessage(), + originalDetails.remoteExceptionStackTrace(), + randomAlphaOfLength(100) + ); + } + default -> throw new IllegalStateException(); + } + } + + private CoordinationDiagnosticsService.CoordinationDiagnosticsStatus mutateStatus( + CoordinationDiagnosticsService.CoordinationDiagnosticsStatus originalStatus + ) { + List notUsedStatuses = Arrays.stream( + CoordinationDiagnosticsService.CoordinationDiagnosticsStatus.values() + ).filter(status -> status.equals(originalStatus) == false).toList(); + return randomFrom(notUsedStatuses); + } + + private CoordinationDiagnosticsService.CoordinationDiagnosticsResult mutateResult( + CoordinationDiagnosticsService.CoordinationDiagnosticsResult originalResult + ) { + switch (randomIntBetween(1, 3)) { + case 1 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsResult( + originalResult.status(), + randomAlphaOfLength(30), + originalResult.details() + ); + } + case 2 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsResult( + getRandomStatus(), + originalResult.summary(), + originalResult.details() + ); + } + case 3 -> { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsResult( + originalResult.status(), + originalResult.summary(), + getRandomDetails() + ); + } + default -> throw new IllegalStateException(); + } + } + + private CoordinationDiagnosticsService.CoordinationDiagnosticsStatus getRandomStatus() { + return randomFrom(CoordinationDiagnosticsService.CoordinationDiagnosticsStatus.values()); + } + + private CoordinationDiagnosticsService.CoordinationDiagnosticsDetails getRandomDetails() { + return new CoordinationDiagnosticsService.CoordinationDiagnosticsDetails( + node1, + List.of(node1, node2), + randomNullableStringOfLengthBetween(0, 30), + randomNullableStringOfLengthBetween(0, 30), + randomAlphaOfLengthBetween(0, 30) + ); + } + + public static String randomNullableStringOfLengthBetween(int minCodeUnits, int maxCodeUnits) { + if (randomBoolean()) { + return null; + } + return randomAlphaOfLengthBetween(minCodeUnits, maxCodeUnits); + } + private static ClusterState createClusterState(DiscoveryNode masterNode) { var routingTableBuilder = RoutingTable.builder(); Metadata.Builder metadataBuilder = Metadata.builder(); diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index 0a64a32d6021a..f0f73791db528 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -245,6 +245,7 @@ public class Constants { "cluster:internal/xpack/ml/trained_models/deployments/stats/get", "cluster:internal/xpack/transform/reset_mode", "cluster:internal/master_history/get", + "cluster:internal/coordination_diagnostics/info", "cluster:internal/formation/info", "cluster:monitor/allocation/explain", "cluster:monitor/async_search/status",