Skip to content

Commit cd0d5c8

Browse files
authored
Creating a transport action for the CoordinationDiagnosticsService (#87984)
This exposes the CoordinationDiagnosticsService (#87672) through a transport action so that it can be called remotely as part of the health API in the event that: (1) there has been no master recently, (2) there are master-eligible nodes in the cluster, (3) none are elected, and (4) the current node is not master eligible.
1 parent 81815f0 commit cd0d5c8

File tree

7 files changed

+449
-3
lines changed

7 files changed

+449
-3
lines changed

docs/changelog/87984.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 87984
2+
summary: Creating a transport action for the `CoordinationDiagnosticsService`
3+
area: Health
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/action/ActionModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction;
1818
import org.elasticsearch.action.admin.cluster.configuration.TransportClearVotingConfigExclusionsAction;
1919
import org.elasticsearch.action.admin.cluster.coordination.ClusterFormationInfoAction;
20+
import org.elasticsearch.action.admin.cluster.coordination.CoordinationDiagnosticsAction;
2021
import org.elasticsearch.action.admin.cluster.coordination.MasterHistoryAction;
2122
import org.elasticsearch.action.admin.cluster.desirednodes.DeleteDesiredNodesAction;
2223
import org.elasticsearch.action.admin.cluster.desirednodes.GetDesiredNodesAction;
@@ -639,6 +640,7 @@ public <Request extends ActionRequest, Response extends ActionResponse> void reg
639640
actions.register(AnalyzeIndexDiskUsageAction.INSTANCE, TransportAnalyzeIndexDiskUsageAction.class);
640641
actions.register(FieldUsageStatsAction.INSTANCE, TransportFieldUsageAction.class);
641642
actions.register(MasterHistoryAction.INSTANCE, MasterHistoryAction.TransportAction.class);
643+
actions.register(CoordinationDiagnosticsAction.INSTANCE, CoordinationDiagnosticsAction.TransportAction.class);
642644

643645
// Indexed scripts
644646
actions.register(PutStoredScriptAction.INSTANCE, TransportPutStoredScriptAction.class);
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.action.admin.cluster.coordination;
10+
11+
import org.elasticsearch.action.ActionListener;
12+
import org.elasticsearch.action.ActionRequest;
13+
import org.elasticsearch.action.ActionRequestValidationException;
14+
import org.elasticsearch.action.ActionResponse;
15+
import org.elasticsearch.action.ActionType;
16+
import org.elasticsearch.action.support.ActionFilters;
17+
import org.elasticsearch.action.support.HandledTransportAction;
18+
import org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService;
19+
import org.elasticsearch.common.inject.Inject;
20+
import org.elasticsearch.common.io.stream.StreamInput;
21+
import org.elasticsearch.common.io.stream.StreamOutput;
22+
import org.elasticsearch.tasks.Task;
23+
import org.elasticsearch.transport.TransportService;
24+
25+
import java.io.IOException;
26+
import java.util.Objects;
27+
28+
import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsResult;
29+
30+
/**
31+
* This action exposes CoordinationDiagnosticsService#diagnoseMasterStability so that a node can get a remote node's view of
32+
* coordination diagnostics (including master stability).
33+
*/
34+
public class CoordinationDiagnosticsAction extends ActionType<CoordinationDiagnosticsAction.Response> {
35+
36+
public static final CoordinationDiagnosticsAction INSTANCE = new CoordinationDiagnosticsAction();
37+
public static final String NAME = "cluster:internal/coordination_diagnostics/info";
38+
39+
private CoordinationDiagnosticsAction() {
40+
super(NAME, CoordinationDiagnosticsAction.Response::new);
41+
}
42+
43+
public static class Request extends ActionRequest {
44+
final boolean explain; // Non-private for testing
45+
46+
public Request(boolean explain) {
47+
this.explain = explain;
48+
}
49+
50+
@Override
51+
public ActionRequestValidationException validate() {
52+
return null;
53+
}
54+
55+
public Request(StreamInput in) throws IOException {
56+
super(in);
57+
this.explain = in.readBoolean();
58+
}
59+
60+
@Override
61+
public void writeTo(StreamOutput out) throws IOException {
62+
super.writeTo(out);
63+
out.writeBoolean(explain);
64+
}
65+
66+
@Override
67+
public boolean equals(Object o) {
68+
if (this == o) return true;
69+
if (o == null || getClass() != o.getClass()) return false;
70+
return explain == ((Request) o).explain;
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return Objects.hashCode(explain);
76+
}
77+
78+
}
79+
80+
public static class Response extends ActionResponse {
81+
82+
private final CoordinationDiagnosticsResult result;
83+
84+
public Response(StreamInput in) throws IOException {
85+
super(in);
86+
result = new CoordinationDiagnosticsResult(in);
87+
}
88+
89+
public Response(CoordinationDiagnosticsResult result) {
90+
this.result = result;
91+
}
92+
93+
public CoordinationDiagnosticsResult getCoordinationDiagnosticsResult() {
94+
return result;
95+
}
96+
97+
@Override
98+
public void writeTo(StreamOutput out) throws IOException {
99+
result.writeTo(out);
100+
}
101+
102+
@Override
103+
public boolean equals(Object o) {
104+
if (this == o) return true;
105+
if (o == null || getClass() != o.getClass()) return false;
106+
CoordinationDiagnosticsAction.Response response = (CoordinationDiagnosticsAction.Response) o;
107+
return result.equals(response.result);
108+
}
109+
110+
@Override
111+
public int hashCode() {
112+
return Objects.hash(result);
113+
}
114+
}
115+
116+
/**
117+
* This transport action calls CoordinationDiagnosticsService#diagnoseMasterStability
118+
*/
119+
public static class TransportAction extends HandledTransportAction<Request, Response> {
120+
private final CoordinationDiagnosticsService coordinationDiagnosticsService;
121+
122+
@Inject
123+
public TransportAction(
124+
TransportService transportService,
125+
ActionFilters actionFilters,
126+
CoordinationDiagnosticsService coordinationDiagnosticsService
127+
) {
128+
super(CoordinationDiagnosticsAction.NAME, transportService, actionFilters, CoordinationDiagnosticsAction.Request::new);
129+
this.coordinationDiagnosticsService = coordinationDiagnosticsService;
130+
}
131+
132+
@Override
133+
protected void doExecute(Task task, CoordinationDiagnosticsAction.Request request, ActionListener<Response> listener) {
134+
listener.onResponse(new Response(coordinationDiagnosticsService.diagnoseMasterStability(request.explain)));
135+
}
136+
}
137+
138+
}

server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsService.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
import org.elasticsearch.cluster.ClusterStateListener;
1515
import org.elasticsearch.cluster.node.DiscoveryNode;
1616
import org.elasticsearch.cluster.service.ClusterService;
17+
import org.elasticsearch.common.io.stream.StreamInput;
18+
import org.elasticsearch.common.io.stream.StreamOutput;
19+
import org.elasticsearch.common.io.stream.Writeable;
1720
import org.elasticsearch.common.settings.Setting;
1821
import org.elasticsearch.core.Nullable;
1922
import org.elasticsearch.core.TimeValue;
2023

24+
import java.io.IOException;
2125
import java.io.PrintWriter;
2226
import java.io.StringWriter;
2327
import java.util.Collection;
@@ -412,13 +416,34 @@ public record CoordinationDiagnosticsResult(
412416
CoordinationDiagnosticsStatus status,
413417
String summary,
414418
CoordinationDiagnosticsDetails details
415-
) {}
419+
) implements Writeable {
416420

417-
public enum CoordinationDiagnosticsStatus {
421+
public CoordinationDiagnosticsResult(StreamInput in) throws IOException {
422+
this(CoordinationDiagnosticsStatus.fromStreamInput(in), in.readString(), new CoordinationDiagnosticsDetails(in));
423+
}
424+
425+
@Override
426+
public void writeTo(StreamOutput out) throws IOException {
427+
status.writeTo(out);
428+
out.writeString(summary);
429+
details.writeTo(out);
430+
}
431+
}
432+
433+
public enum CoordinationDiagnosticsStatus implements Writeable {
418434
GREEN,
419435
UNKNOWN,
420436
YELLOW,
421437
RED;
438+
439+
@Override
440+
public void writeTo(StreamOutput out) throws IOException {
441+
out.writeEnum(this);
442+
}
443+
444+
public static CoordinationDiagnosticsStatus fromStreamInput(StreamInput in) throws IOException {
445+
return in.readEnum(CoordinationDiagnosticsStatus.class);
446+
}
422447
}
423448

424449
public record CoordinationDiagnosticsDetails(
@@ -427,7 +452,7 @@ public record CoordinationDiagnosticsDetails(
427452
@Nullable String remoteExceptionMessage,
428453
@Nullable String remoteExceptionStackTrace,
429454
@Nullable String clusterFormationDescription
430-
) {
455+
) implements Writeable {
431456

432457
public CoordinationDiagnosticsDetails(DiscoveryNode currentMaster, List<DiscoveryNode> recentMasters) {
433458
this(currentMaster, recentMasters, null, null, null);
@@ -437,6 +462,32 @@ public CoordinationDiagnosticsDetails(DiscoveryNode currentMaster, Exception rem
437462
this(currentMaster, null, remoteException == null ? null : remoteException.getMessage(), getStackTrace(remoteException), null);
438463
}
439464

465+
public CoordinationDiagnosticsDetails(StreamInput in) throws IOException {
466+
this(readCurrentMaster(in), readRecentMasters(in), in.readOptionalString(), in.readOptionalString(), in.readOptionalString());
467+
}
468+
469+
private static DiscoveryNode readCurrentMaster(StreamInput in) throws IOException {
470+
boolean hasCurrentMaster = in.readBoolean();
471+
DiscoveryNode currentMaster;
472+
if (hasCurrentMaster) {
473+
currentMaster = new DiscoveryNode(in);
474+
} else {
475+
currentMaster = null;
476+
}
477+
return currentMaster;
478+
}
479+
480+
private static List<DiscoveryNode> readRecentMasters(StreamInput in) throws IOException {
481+
boolean hasRecentMasters = in.readBoolean();
482+
List<DiscoveryNode> recentMasters;
483+
if (hasRecentMasters) {
484+
recentMasters = in.readImmutableList(DiscoveryNode::new);
485+
} else {
486+
recentMasters = null;
487+
}
488+
return recentMasters;
489+
}
490+
440491
private static String getStackTrace(Exception e) {
441492
if (e == null) {
442493
return null;
@@ -447,5 +498,25 @@ private static String getStackTrace(Exception e) {
447498
}
448499

449500
public static final CoordinationDiagnosticsDetails EMPTY = new CoordinationDiagnosticsDetails(null, null, null, null, null);
501+
502+
@Override
503+
public void writeTo(StreamOutput out) throws IOException {
504+
if (currentMaster == null) {
505+
out.writeBoolean(false);
506+
} else {
507+
out.writeBoolean(true);
508+
currentMaster.writeTo(out);
509+
}
510+
if (recentMasters == null) {
511+
out.writeBoolean(false);
512+
} else {
513+
out.writeBoolean(true);
514+
out.writeList(recentMasters);
515+
}
516+
out.writeOptionalString(remoteExceptionMessage);
517+
out.writeOptionalString(remoteExceptionStackTrace);
518+
out.writeOptionalString(clusterFormationDescription);
519+
}
520+
450521
}
451522
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.action.admin.cluster.coordination;
10+
11+
import org.elasticsearch.Version;
12+
import org.elasticsearch.cluster.node.DiscoveryNode;
13+
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
14+
import org.elasticsearch.test.ESTestCase;
15+
import org.elasticsearch.test.EqualsHashCodeTestUtils;
16+
17+
import java.util.Collections;
18+
import java.util.List;
19+
import java.util.UUID;
20+
21+
import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsDetails;
22+
import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsResult;
23+
import static org.elasticsearch.cluster.coordination.CoordinationDiagnosticsService.CoordinationDiagnosticsStatus;
24+
25+
public class CoordinationDiagnosticsActionTests extends ESTestCase {
26+
27+
public void testSerialization() {
28+
DiscoveryNode node1 = new DiscoveryNode(
29+
"node1",
30+
UUID.randomUUID().toString(),
31+
buildNewFakeTransportAddress(),
32+
Collections.emptyMap(),
33+
DiscoveryNodeRole.roles(),
34+
Version.CURRENT
35+
);
36+
DiscoveryNode node2 = new DiscoveryNode(
37+
"node2",
38+
UUID.randomUUID().toString(),
39+
buildNewFakeTransportAddress(),
40+
Collections.emptyMap(),
41+
DiscoveryNodeRole.roles(),
42+
Version.CURRENT
43+
);
44+
CoordinationDiagnosticsDetails details = new CoordinationDiagnosticsDetails(
45+
node1,
46+
List.of(node1, node2),
47+
randomAlphaOfLengthBetween(0, 30),
48+
randomAlphaOfLengthBetween(0, 30),
49+
randomAlphaOfLengthBetween(0, 30)
50+
);
51+
CoordinationDiagnosticsResult result = new CoordinationDiagnosticsResult(
52+
randomFrom(CoordinationDiagnosticsStatus.values()),
53+
randomAlphaOfLength(100),
54+
details
55+
);
56+
CoordinationDiagnosticsAction.Response response = new CoordinationDiagnosticsAction.Response(result);
57+
EqualsHashCodeTestUtils.checkEqualsAndHashCode(
58+
response,
59+
history -> copyWriteable(history, writableRegistry(), CoordinationDiagnosticsAction.Response::new),
60+
this::mutateResponse
61+
);
62+
63+
CoordinationDiagnosticsAction.Request request = new CoordinationDiagnosticsAction.Request(randomBoolean());
64+
EqualsHashCodeTestUtils.checkEqualsAndHashCode(
65+
request,
66+
history -> copyWriteable(history, writableRegistry(), CoordinationDiagnosticsAction.Request::new),
67+
this::mutateRequest
68+
);
69+
}
70+
71+
private CoordinationDiagnosticsAction.Request mutateRequest(CoordinationDiagnosticsAction.Request originalRequest) {
72+
return new CoordinationDiagnosticsAction.Request(originalRequest.explain == false);
73+
}
74+
75+
private CoordinationDiagnosticsAction.Response mutateResponse(CoordinationDiagnosticsAction.Response originalResponse) {
76+
CoordinationDiagnosticsResult originalResult = originalResponse.getCoordinationDiagnosticsResult();
77+
return new CoordinationDiagnosticsAction.Response(
78+
new CoordinationDiagnosticsResult(originalResult.status(), randomAlphaOfLength(100), originalResult.details())
79+
);
80+
}
81+
}

0 commit comments

Comments
 (0)