Skip to content

Commit 48dc6c3

Browse files
[Zen2] Implement Tombstone REST APIs (#36007)
* [Zen2] Implement Tombstone REST APIs * Adds REST API for withdrawing votes and clearing vote withdrawls * Tests added to Netty4 module since we need a real Network impl. for Http endpoints
1 parent 7f25718 commit 48dc6c3

File tree

6 files changed

+300
-2
lines changed

6 files changed

+300
-2
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.rest.discovery;
21+
22+
import org.apache.http.HttpHost;
23+
import org.elasticsearch.ESNetty4IntegTestCase;
24+
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
25+
import org.elasticsearch.client.Client;
26+
import org.elasticsearch.client.Node;
27+
import org.elasticsearch.client.Request;
28+
import org.elasticsearch.client.Response;
29+
import org.elasticsearch.client.ResponseException;
30+
import org.elasticsearch.client.RestClient;
31+
import org.elasticsearch.cluster.coordination.ClusterBootstrapService;
32+
import org.elasticsearch.cluster.metadata.IndexMetaData;
33+
import org.elasticsearch.cluster.routing.UnassignedInfo;
34+
import org.elasticsearch.common.Priority;
35+
import org.elasticsearch.common.settings.Settings;
36+
import org.elasticsearch.common.unit.TimeValue;
37+
import org.elasticsearch.discovery.zen.ElectMasterService;
38+
import org.elasticsearch.http.HttpServerTransport;
39+
import org.elasticsearch.test.ESIntegTestCase;
40+
import org.elasticsearch.test.InternalTestCluster;
41+
import org.elasticsearch.test.discovery.TestZenDiscovery;
42+
import org.hamcrest.Matchers;
43+
44+
import java.io.IOException;
45+
import java.util.Collections;
46+
import java.util.List;
47+
48+
import static org.hamcrest.core.Is.is;
49+
50+
// These tests are here today so they have access to a proper REST client. They cannot be in :server:integTest since the REST client needs a
51+
// proper transport implementation, and they cannot be REST tests today since they need to restart nodes. When #35599 and friends land we
52+
// should be able to move these tests to run against a proper cluster instead. TODO do this.
53+
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, transportClientRatio = 0, autoMinMasterNodes = false)
54+
public class Zen2RestApiIT extends ESNetty4IntegTestCase {
55+
56+
@Override
57+
protected Settings nodeSettings(int nodeOrdinal) {
58+
return Settings.builder().put(super.nodeSettings(nodeOrdinal))
59+
.put(TestZenDiscovery.USE_ZEN2.getKey(), true)
60+
.put(ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING.getKey(), Integer.MAX_VALUE)
61+
.put(ClusterBootstrapService.INITIAL_MASTER_NODE_COUNT_SETTING.getKey(), 2)
62+
.build();
63+
}
64+
65+
@Override
66+
protected boolean addMockHttpTransport() {
67+
return false; // enable http
68+
}
69+
70+
public void testRollingRestartOfTwoNodeCluster() throws Exception {
71+
final List<String> nodes = internalCluster().startNodes(2);
72+
createIndex("test",
73+
Settings.builder()
74+
.put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), TimeValue.ZERO) // assign shards
75+
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 2) // causes rebalancing
76+
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)
77+
.build());
78+
ensureGreen("test");
79+
80+
RestClient restClient = getRestClient();
81+
82+
internalCluster().rollingRestart(new InternalTestCluster.RestartCallback() {
83+
@Override
84+
public void doAfterNodes(int n, Client client) throws IOException {
85+
ensureGreen("test");
86+
Response response =
87+
restClient.performRequest(new Request("POST", "/_cluster/withdrawn_votes/" + internalCluster().getNodeNames()[n]));
88+
assertThat(response.getStatusLine().getStatusCode(), is(200));
89+
}
90+
91+
@Override
92+
public Settings onNodeStopped(String nodeName) throws IOException {
93+
String viaNode = randomValueOtherThan(nodeName, () -> randomFrom(nodes));
94+
95+
List<Node> allNodes = restClient.getNodes();
96+
try {
97+
restClient.setNodes(
98+
Collections.singletonList(
99+
new Node(
100+
HttpHost.create(
101+
internalCluster().getInstance(HttpServerTransport.class, viaNode)
102+
.boundAddress().publishAddress().toString()
103+
)
104+
)
105+
)
106+
);
107+
Response deleteResponse = restClient.performRequest(new Request("DELETE", "/_cluster/withdrawn_votes"));
108+
assertThat(deleteResponse.getStatusLine().getStatusCode(), is(200));
109+
110+
ClusterHealthResponse clusterHealthResponse = client(viaNode).admin().cluster().prepareHealth()
111+
.setWaitForEvents(Priority.LANGUID)
112+
.setWaitForNodes(Integer.toString(1))
113+
.setTimeout(TimeValue.timeValueSeconds(30L))
114+
.setWaitForYellowStatus()
115+
.get();
116+
assertFalse(nodeName, clusterHealthResponse.isTimedOut());
117+
return Settings.EMPTY;
118+
} finally {
119+
restClient.setNodes(allNodes);
120+
}
121+
}
122+
});
123+
ensureStableCluster(2);
124+
ensureGreen("test");
125+
assertThat(internalCluster().size(), is(2));
126+
}
127+
128+
public void testClearVotingTombstonesNotWaitingForRemoval() throws Exception {
129+
List<String> nodes = internalCluster().startNodes(3);
130+
RestClient restClient = getRestClient();
131+
Response response = restClient.performRequest(new Request("POST", "/_cluster/withdrawn_votes/" + nodes.get(2)));
132+
assertThat(response.getStatusLine().getStatusCode(), is(200));
133+
assertThat(response.getEntity().getContentLength(), is(0L));
134+
Response deleteResponse = restClient.performRequest(new Request("DELETE", "/_cluster/withdrawn_votes/?wait_for_removal=false"));
135+
assertThat(deleteResponse.getStatusLine().getStatusCode(), is(200));
136+
assertThat(deleteResponse.getEntity().getContentLength(), is(0L));
137+
}
138+
139+
public void testClearVotingTombstonesWaitingForRemoval() throws Exception {
140+
List<String> nodes = internalCluster().startNodes(3);
141+
RestClient restClient = getRestClient();
142+
String nodeToWithdraw = nodes.get(randomIntBetween(0, 2));
143+
Response response = restClient.performRequest(new Request("POST", "/_cluster/withdrawn_votes/" + nodeToWithdraw));
144+
assertThat(response.getStatusLine().getStatusCode(), is(200));
145+
assertThat(response.getEntity().getContentLength(), is(0L));
146+
internalCluster().stopRandomNode(InternalTestCluster.nameFilter(nodeToWithdraw));
147+
Response deleteResponse = restClient.performRequest(new Request("DELETE", "/_cluster/withdrawn_votes"));
148+
assertThat(deleteResponse.getStatusLine().getStatusCode(), is(200));
149+
assertThat(deleteResponse.getEntity().getContentLength(), is(0L));
150+
}
151+
152+
public void testFailsOnUnknownNode() throws Exception {
153+
internalCluster().startNodes(3);
154+
RestClient restClient = getRestClient();
155+
try {
156+
restClient.performRequest(new Request("POST", "/_cluster/withdrawn_votes/invalid"));
157+
fail("Invalid node name should throw.");
158+
} catch (ResponseException e) {
159+
assertThat(e.getResponse().getStatusLine().getStatusCode(), is(400));
160+
assertThat(
161+
e.getCause().getMessage(),
162+
Matchers.containsString("add voting tombstones request for [invalid] matched no master-eligible nodes")
163+
);
164+
}
165+
}
166+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
import org.elasticsearch.rest.action.RestFieldCapabilitiesAction;
226226
import org.elasticsearch.rest.action.RestMainAction;
227227
import org.elasticsearch.rest.action.admin.cluster.RestCancelTasksAction;
228+
import org.elasticsearch.rest.action.admin.cluster.RestClearVotingTombstonesAction;
228229
import org.elasticsearch.rest.action.admin.cluster.RestClusterAllocationExplainAction;
229230
import org.elasticsearch.rest.action.admin.cluster.RestClusterGetSettingsAction;
230231
import org.elasticsearch.rest.action.admin.cluster.RestClusterHealthAction;
@@ -254,6 +255,7 @@
254255
import org.elasticsearch.rest.action.admin.cluster.RestRestoreSnapshotAction;
255256
import org.elasticsearch.rest.action.admin.cluster.RestSnapshotsStatusAction;
256257
import org.elasticsearch.rest.action.admin.cluster.RestVerifyRepositoryAction;
258+
import org.elasticsearch.rest.action.admin.cluster.RestAddVotingTombstonesAction;
257259
import org.elasticsearch.rest.action.admin.indices.RestAnalyzeAction;
258260
import org.elasticsearch.rest.action.admin.indices.RestClearIndicesCacheAction;
259261
import org.elasticsearch.rest.action.admin.indices.RestCloseIndexAction;
@@ -543,6 +545,8 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster) {
543545
catActions.add((AbstractCatAction) a);
544546
}
545547
};
548+
registerHandler.accept(new RestAddVotingTombstonesAction(settings, restController));
549+
registerHandler.accept(new RestClearVotingTombstonesAction(settings, restController));
546550
registerHandler.accept(new RestMainAction(settings, restController));
547551
registerHandler.accept(new RestNodesInfoAction(settings, restController, settingsFilter));
548552
registerHandler.accept(new RestRemoteClusterInfoAction(settings, restController));

server/src/main/java/org/elasticsearch/action/admin/cluster/configuration/AddVotingTombstonesResponse.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@
2121
import org.elasticsearch.action.ActionResponse;
2222
import org.elasticsearch.common.io.stream.StreamInput;
2323
import org.elasticsearch.common.io.stream.StreamOutput;
24+
import org.elasticsearch.common.xcontent.ToXContentObject;
25+
import org.elasticsearch.common.xcontent.XContentBuilder;
2426

2527
import java.io.IOException;
2628

2729
/**
2830
* A response to {@link AddVotingTombstonesRequest} indicating that voting tombstones have been added for the requested nodes and these
2931
* nodes have been removed from the voting configuration.
3032
*/
31-
public class AddVotingTombstonesResponse extends ActionResponse {
33+
public class AddVotingTombstonesResponse extends ActionResponse implements ToXContentObject {
3234

3335
public AddVotingTombstonesResponse() {
3436
}
@@ -46,4 +48,9 @@ public void readFrom(StreamInput in) throws IOException {
4648
public void writeTo(StreamOutput out) throws IOException {
4749
super.writeTo(out);
4850
}
51+
52+
@Override
53+
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
54+
return builder;
55+
}
4956
}

server/src/main/java/org/elasticsearch/action/admin/cluster/configuration/ClearVotingTombstonesResponse.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
import org.elasticsearch.action.ActionResponse;
2222
import org.elasticsearch.common.io.stream.StreamInput;
2323
import org.elasticsearch.common.io.stream.StreamOutput;
24+
import org.elasticsearch.common.xcontent.ToXContentObject;
25+
import org.elasticsearch.common.xcontent.XContentBuilder;
2426

2527
import java.io.IOException;
2628

2729
/**
2830
* A response to {@link ClearVotingTombstonesRequest} indicating that voting tombstones have been cleared from the cluster state.
2931
*/
30-
public class ClearVotingTombstonesResponse extends ActionResponse {
32+
public class ClearVotingTombstonesResponse extends ActionResponse implements ToXContentObject {
3133
public ClearVotingTombstonesResponse() {
3234
}
3335

@@ -44,4 +46,9 @@ public void readFrom(StreamInput in) throws IOException {
4446
public void writeTo(StreamOutput out) throws IOException {
4547
super.writeTo(out);
4648
}
49+
50+
@Override
51+
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
52+
return builder;
53+
}
4754
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.rest.action.admin.cluster;
21+
22+
import org.elasticsearch.action.admin.cluster.configuration.AddVotingTombstonesAction;
23+
import org.elasticsearch.action.admin.cluster.configuration.AddVotingTombstonesRequest;
24+
import org.elasticsearch.client.node.NodeClient;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.common.unit.TimeValue;
27+
import org.elasticsearch.rest.BaseRestHandler;
28+
import org.elasticsearch.rest.RestController;
29+
import org.elasticsearch.rest.RestRequest;
30+
import org.elasticsearch.rest.action.RestToXContentListener;
31+
32+
import java.io.IOException;
33+
34+
public class RestAddVotingTombstonesAction extends BaseRestHandler {
35+
36+
private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(30L);
37+
38+
public RestAddVotingTombstonesAction(Settings settings, RestController controller) {
39+
super(settings);
40+
controller.registerHandler(RestRequest.Method.POST, "/_cluster/withdrawn_votes/{node_name}", this);
41+
}
42+
43+
@Override
44+
public String getName() {
45+
return "add_voting_tombstones_action";
46+
}
47+
48+
@Override
49+
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
50+
String nodeName = request.param("node_name");
51+
AddVotingTombstonesRequest addVotingTombstonesRequest = new AddVotingTombstonesRequest(
52+
new String[]{nodeName},
53+
TimeValue.parseTimeValue(request.param("timeout"), DEFAULT_TIMEOUT, getClass().getSimpleName() + ".timeout")
54+
);
55+
return channel -> client.execute(
56+
AddVotingTombstonesAction.INSTANCE,
57+
addVotingTombstonesRequest,
58+
new RestToXContentListener<>(channel)
59+
);
60+
}
61+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.rest.action.admin.cluster;
21+
22+
import org.elasticsearch.action.admin.cluster.configuration.ClearVotingTombstonesAction;
23+
import org.elasticsearch.action.admin.cluster.configuration.ClearVotingTombstonesRequest;
24+
import org.elasticsearch.client.node.NodeClient;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.rest.BaseRestHandler;
27+
import org.elasticsearch.rest.RestController;
28+
import org.elasticsearch.rest.RestRequest;
29+
import org.elasticsearch.rest.action.RestToXContentListener;
30+
31+
import java.io.IOException;
32+
33+
public class RestClearVotingTombstonesAction extends BaseRestHandler {
34+
35+
public RestClearVotingTombstonesAction(Settings settings, RestController controller) {
36+
super(settings);
37+
controller.registerHandler(RestRequest.Method.DELETE, "/_cluster/withdrawn_votes", this);
38+
}
39+
40+
@Override
41+
public String getName() {
42+
return "clear_voting_tombstones_action";
43+
}
44+
45+
@Override
46+
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
47+
ClearVotingTombstonesRequest req = new ClearVotingTombstonesRequest();
48+
if (request.hasParam("wait_for_removal")) {
49+
req.setWaitForRemoval(request.paramAsBoolean("wait_for_removal", true));
50+
}
51+
return channel -> client.execute(ClearVotingTombstonesAction.INSTANCE, req, new RestToXContentListener<>(channel));
52+
}
53+
}

0 commit comments

Comments
 (0)