Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voting config exclusions should work with absent nodes #50836

Merged
merged 33 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c3d4615
Voting config exclusions should work with absent nodes
zacharymorn Jan 9, 2020
7881e7b
Merge branch 'master' into issue-47990
zacharymorn Jan 17, 2020
39eb2a1
Add new APIs to add voting config exclusion just based on node id or …
zacharymorn Jan 17, 2020
c3eb5a1
Address feedback comment
zacharymorn Feb 28, 2020
ad41573
Address feedback
zacharymorn Mar 4, 2020
b085c2c
Address comment
zacharymorn Mar 4, 2020
57cbc47
Update nodeId for VotingConfigExclusion when node with matching name …
zacharymorn Mar 5, 2020
e08a8a1
Add test cases to AddVotingConfigExclusionsRequestTests
zacharymorn Mar 6, 2020
d325123
Add assertion for voting config exclusion in cluster state
zacharymorn Mar 7, 2020
bede9a0
Add test cases to TransportAddVotingConfigExclusionsActionTests
zacharymorn Mar 7, 2020
fb337f2
Add test to NodeJoinTests
zacharymorn Mar 10, 2020
5e28294
Add test to CooridnatorTests
zacharymorn Mar 10, 2020
49f749c
Inline deprecation message
zacharymorn Mar 10, 2020
a174c7c
Merge branch 'master' into issue-47990
zacharymorn Mar 10, 2020
2d37e5c
Address some comments that can be fixed quickly
zacharymorn Mar 26, 2020
0fb29c3
Make Coordinator#validVotingConfigExclusionState package static for t…
zacharymorn Mar 26, 2020
aaa0f89
Refactoring for node resolution logic and NodeJoinTest
zacharymorn Mar 27, 2020
02a3533
Refactor out deprecated AddVotingConfigExclusionsRequest constructor
zacharymorn Mar 27, 2020
53f133c
Fix checkstyle and tests
zacharymorn Mar 27, 2020
5fe180e
Merge branch 'master' into issue-47990
zacharymorn Mar 27, 2020
0f6dd6f
Fix test failure due to misnomer
zacharymorn Mar 27, 2020
b0d0a97
Revert "Refactor out deprecated AddVotingConfigExclusionsRequest cons…
zacharymorn Apr 9, 2020
5c7a226
Migrate some tests that use nodeDescriptions to using nodeNames
zacharymorn Apr 9, 2020
c6fbce8
Fix style
zacharymorn Apr 9, 2020
fcd3912
Merge branch 'master' into issue-47990
zacharymorn Apr 9, 2020
a2e6247
Revert "Merge branch 'master' into issue-47990"
zacharymorn Apr 9, 2020
ccf6c73
Merge branch 'master' into issue-47990
zacharymorn Apr 9, 2020
2a01d70
Fix spacing
zacharymorn Apr 9, 2020
6d6353e
Update server/src/main/java/org/elasticsearch/cluster/coordination/Jo…
zacharymorn Apr 10, 2020
99a584d
Update server/src/main/java/org/elasticsearch/action/admin/cluster/co…
zacharymorn Apr 10, 2020
89ab437
Update server/src/test/java/org/elasticsearch/cluster/coordination/Co…
zacharymorn Apr 10, 2020
fd64f4b
Address feedback to use Map<String, DiscoveryNode> for existing nodes
zacharymorn Apr 10, 2020
51f74bf
Fix checkstyle
zacharymorn Apr 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
*/
package org.elasticsearch.action.admin.cluster.configuration;

import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.master.MasterNodeRequest;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.coordination.CoordinationMetaData.VotingConfigExclusion;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.iterable.Iterables;
import org.elasticsearch.common.util.set.Sets;

import java.io.IOException;
import java.util.Arrays;
Expand All @@ -39,6 +43,8 @@
*/
public class AddVotingConfigExclusionsRequest extends MasterNodeRequest<AddVotingConfigExclusionsRequest> {
private final String[] nodeDescriptions;
private String[] nodeIds = null;
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
private String[] nodeNames = null;
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
private final TimeValue timeout;

/**
Expand All @@ -47,7 +53,7 @@ public class AddVotingConfigExclusionsRequest extends MasterNodeRequest<AddVotin
* @param nodeDescriptions Descriptions of the nodes to add - see {@link DiscoveryNodes#resolveNodes(String...)}
*/
public AddVotingConfigExclusionsRequest(String[] nodeDescriptions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor is only used in tests, and it looks like we could migrate all of those tests over to using node names instead of node descriptions. Some of them would also be neater if we used a varargs:

Suggested change
public AddVotingConfigExclusionsRequest(String[] nodeDescriptions) {
public AddVotingConfigExclusionsRequest(String... nodeNames) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we migrate these tests now to use node names instead of descriptions, I'm a bit concerned that we may not have tests to prove that the changes are still backward compatible and don't have bugs that may break logic based on nodeDescriptions, before it is fully migrated to nodeIds / nodeNames. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you're right, I didn't quite mean "all" these tests. We should comprehensively test the different kinds of node resolution by strengthening AddVotingConfigExclusionsRequestTests. The other tests can move over to node names without loss of coverage IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't resolved yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed this earlier. Done in commit 02a3533

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed this useful constructor in 02a3533 and added a lot of noise to the tests as a result. Could you follow my suggestion above instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry when I re-read this thread I somehow got the wrong idea that this constructor need to be removed. I reverted that commit and tried again in commit 5c7a226. Could you let me know if this looks good?

this(nodeDescriptions, TimeValue.timeValueSeconds(30));
this(nodeDescriptions, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, TimeValue.timeValueSeconds(30));
}

/**
Expand All @@ -56,32 +62,72 @@ public AddVotingConfigExclusionsRequest(String[] nodeDescriptions) {
* @param nodeDescriptions Descriptions of the nodes whose exclusions to add - see {@link DiscoveryNodes#resolveNodes(String...)}.
* @param timeout How long to wait for the added exclusions to take effect and be removed from the voting configuration.
*/
public AddVotingConfigExclusionsRequest(String[] nodeDescriptions, TimeValue timeout) {
public AddVotingConfigExclusionsRequest(String[] nodeDescriptions, String[] nodeIds, String[] nodeNames, TimeValue timeout) {
if (timeout.compareTo(TimeValue.ZERO) < 0) {
throw new IllegalArgumentException("timeout [" + timeout + "] must be non-negative");
}
this.nodeDescriptions = nodeDescriptions;
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
this.nodeIds = nodeIds;
this.nodeNames = nodeNames;
this.timeout = timeout;
}

public AddVotingConfigExclusionsRequest(StreamInput in) throws IOException {
super(in);
// TODO should this be removed in the latest version where nodeIds and nodeNames are used?
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
nodeDescriptions = in.readStringArray();
// TODO which version to use here?
if (in.getVersion() == Version.V_EMPTY) {
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
nodeIds = in.readStringArray();
nodeNames = in.readStringArray();
}
timeout = in.readTimeValue();
}

Set<VotingConfigExclusion> resolveVotingConfigExclusions(ClusterState currentState) {
final DiscoveryNodes allNodes = currentState.nodes();
final Set<VotingConfigExclusion> resolvedNodes = Arrays.stream(allNodes.resolveNodes(nodeDescriptions))
Set<VotingConfigExclusion> allProcessedNodes = null;

if (nodeDescriptions.length >= 1) {
allProcessedNodes = Arrays.stream(allNodes.resolveNodes(nodeDescriptions)).map(allNodes::get)
.filter(DiscoveryNode::isMasterNode).map(VotingConfigExclusion::new).collect(Collectors.toSet());

if (allProcessedNodes.isEmpty()) {
throw new IllegalArgumentException("add voting config exclusions request for " + Arrays.asList(nodeDescriptions)
+ " matched no master-eligible nodes");
}
}
else {
DiscoveryNodes.NodeResolutionResults nodeResolutionResults;
Set<VotingConfigExclusion> resolvedNodes;
Set<VotingConfigExclusion> unresolvedNodes;

if (nodeIds.length >= 1) {
nodeResolutionResults = allNodes.resolveNodesExact(true, nodeIds);

unresolvedNodes = Arrays.stream(nodeResolutionResults.getUnresolvedNodes())
.map(nodeId -> new VotingConfigExclusion(nodeId, "")).collect(Collectors.toSet());
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
}
else {
nodeResolutionResults = allNodes.resolveNodesExact(false, nodeNames);

unresolvedNodes = Arrays.stream(nodeResolutionResults.getUnresolvedNodes())
.map(nodeName -> new VotingConfigExclusion("", nodeName)).collect(Collectors.toSet());
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
}

resolvedNodes = Arrays.stream(nodeResolutionResults.getResolvedNodes())
.map(allNodes::get).filter(DiscoveryNode::isMasterNode).map(VotingConfigExclusion::new).collect(Collectors.toSet());

if (resolvedNodes.isEmpty()) {
throw new IllegalArgumentException("add voting config exclusions request for " + Arrays.asList(nodeDescriptions)
+ " matched no master-eligible nodes");
allProcessedNodes = Sets.newHashSet(Iterables.concat(resolvedNodes, unresolvedNodes));

if (allProcessedNodes.isEmpty()) {
throw new IllegalArgumentException("add voting config exclusions request for nodeIds " + Arrays.asList(nodeIds) +
" or nodeNames " + Arrays.asList(nodeNames) + " matched no master-eligible nodes or absent nodes");
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
}
}

resolvedNodes.removeIf(n -> currentState.getVotingConfigExclusions().contains(n));
return resolvedNodes;
allProcessedNodes.removeIf(n -> currentState.getVotingConfigExclusions().contains(n));
return allProcessedNodes;
}

Set<VotingConfigExclusion> resolveVotingConfigExclusionsAndCheckMaximum(ClusterState currentState, int maxExclusionsCount,
Expand All @@ -106,6 +152,20 @@ public String[] getNodeDescriptions() {
return nodeDescriptions;
}

/**
* @return ids of the nodes for whom to add voting config exclusions.
*/
public String[] getNodeIds() {
return nodeIds;
}

/**
* @return names of the nodes for whom to add voting config exclusions.
*/
public String[] getNodeNames() {
return nodeNames;
}

/**
* @return how long to wait after adding the exclusions for the nodes to be removed from the voting configuration.
*/
Expand All @@ -121,15 +181,24 @@ public ActionRequestValidationException validate() {
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
// TODO should this be removed in the latest version where nodeIds and nodeNames are used?
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
out.writeStringArray(nodeDescriptions);
// TODO which version to use here?
if (out.getVersion() == Version.V_EMPTY) {
out.writeStringArray(nodeIds);
out.writeStringArray(nodeNames);

}
out.writeTimeValue(timeout);
}

@Override
public String toString() {
return "AddVotingConfigExclusionsRequest{" +
"nodeDescriptions=" + Arrays.asList(nodeDescriptions) +
", timeout=" + timeout +
"nodeDescriptions=" + Arrays.asList(nodeDescriptions) + ", " +
"nodeIds=" + Arrays.asList(nodeIds) + ", " +
"nodeNames=" + Arrays.asList(nodeNames) + ", " +
"timeout=" + timeout +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public static class Builder {
public Builder() {

}

public Builder(CoordinationMetaData state) {
this.term = state.term;
this.lastCommittedConfiguration = state.lastCommittedConfiguration;
Expand Down Expand Up @@ -233,7 +233,8 @@ public static class VotingConfigExclusion implements Writeable, ToXContentFragme
private final String nodeName;

public VotingConfigExclusion(DiscoveryNode node) {
this(node.getId(), node.getName());
this.nodeId = node.getId();
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
this.nodeName = node.getName();
}

public VotingConfigExclusion(StreamInput in) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -317,6 +318,63 @@ public DiscoveryNode resolveNode(String node) {
return nodes.get(resolvedNodeIds[0]);
}

public static class NodeResolutionResults {
private String[] resolvedNodes;
private String[] unresolvedNodes;

public NodeResolutionResults(String[] resolvedNodes, String[] unresolvedNodes) {
this.resolvedNodes = resolvedNodes;
this.unresolvedNodes = unresolvedNodes;
}

public String[] getResolvedNodes() {
return resolvedNodes;
}

public String[] getUnresolvedNodes() {
return unresolvedNodes;
}
}

public NodeResolutionResults resolveNodesExact(boolean isNodeIds, String... nodes) {
if (nodes == null || nodes.length == 0) {
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
return new NodeResolutionResults(StreamSupport.stream(this.spliterator(), false)
.map(DiscoveryNode::getId).toArray(String[]::new), new String[0]);
} else {
ObjectHashSet<String> resolvedNodes = new ObjectHashSet<>(nodes.length);
ObjectHashSet<String> unresolvedNodes = new ObjectHashSet<>();

if (isNodeIds) {
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
for (String nodeId : nodes) {
if (nodeExists(nodeId)) {
resolvedNodes.add(nodeId);
}
else {
unresolvedNodes.add(nodeId);
}
}
}
else {
Map<String, String> existingNodesNameId = new HashMap<>();
for (DiscoveryNode node : this) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just the master-eligible nodes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry a question just came up when I looked at this again. When we resolve by nodeId, we use ALL existing nodes to check if it exists, not just the master-eligible ones. Shall we keep this behavior the same for resolving by node name as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll open a new comment thread on the newly-moved code.

existingNodesNameId.put(node.getName(), node.getId());
}

for (String nodeName : nodes) {
if (existingNodesNameId.containsKey(nodeName)){
resolvedNodes.add(existingNodesNameId.get(nodeName));
}
else {
unresolvedNodes.add(nodeName);
}
}
}

return new NodeResolutionResults(resolvedNodes.toArray(String.class), unresolvedNodes.toArray(String.class));
}
}


/**
* resolves a set of node "descriptions" to concrete and existing node ids. "descriptions" can be (resolved in this order):
* - "_local" or "_master" for the relevant nodes
Expand All @@ -330,6 +388,7 @@ public String[] resolveNodes(String... nodes) {
return StreamSupport.stream(this.spliterator(), false).map(DiscoveryNode::getId).toArray(String[]::new);
} else {
ObjectHashSet<String> resolvedNodesIds = new ObjectHashSet<>(nodes.length);

DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
for (String nodeId : nodes) {
if (nodeId.equals("_local")) {
String localNodeId = getLocalNodeId();
Expand All @@ -346,9 +405,9 @@ public String[] resolveNodes(String... nodes) {
} else {
for (DiscoveryNode node : this) {
if ("_all".equals(nodeId)
|| Regex.simpleMatch(nodeId, node.getName())
|| Regex.simpleMatch(nodeId, node.getHostAddress())
|| Regex.simpleMatch(nodeId, node.getHostName())) {
|| Regex.simpleMatch(nodeId, node.getName())
|| Regex.simpleMatch(nodeId, node.getHostAddress())
|| Regex.simpleMatch(nodeId, node.getHostName())) {
resolvedNodesIds.add(node.getId());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,28 @@
import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsRequest;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;

public class RestAddVotingConfigExclusionAction extends BaseRestHandler {

private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(30L);
private static final Logger logger = LogManager.getLogger(RestAddVotingConfigExclusionAction.class);
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(logger);

public RestAddVotingConfigExclusionAction(RestController controller) {
// TODO This API is being deprecated.
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
controller.registerHandler(RestRequest.Method.POST, "/_cluster/voting_config_exclusions/{node_name}", this);

controller.registerHandler(RestRequest.Method.POST, "/_cluster/voting_config_exclusions", this);
}

@Override
Expand All @@ -55,10 +63,53 @@ protected RestChannelConsumer prepareRequest(final RestRequest request, final No
}

AddVotingConfigExclusionsRequest resolveVotingConfigExclusionsRequest(final RestRequest request) {
String nodeName = request.param("node_name");
String deprecatedNodeDescription = null;
String nodeIds = null;
String nodeNames = null;

if (request.hasParam("node_name")) {
DEPRECATION_LOGGER.deprecatedAndMaybeLog("add_voting_config_exclusion",
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
"Using [node_name] for adding voting config exclustion will be removed in a future version. " +
"Please use [node_ids] or [node_names] instead");
deprecatedNodeDescription = request.param("node_name");
}

if (request.hasParam("node_ids")){
nodeIds = request.param("node_ids");
}

if (request.hasParam("node_names")){
nodeNames = request.param("node_names");
}

if(!oneAndonlyOneIsSet(deprecatedNodeDescription, nodeIds, nodeNames)) {
DaveCTurner marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("Please set node identifiers correctly. " +
"One and only one of [node_name], [node_names] and [node_ids] has to be set");
}

return new AddVotingConfigExclusionsRequest(
Strings.splitStringByCommaToArray(nodeName),
Strings.splitStringByCommaToArray(deprecatedNodeDescription),
Strings.splitStringByCommaToArray(nodeIds),
Strings.splitStringByCommaToArray(nodeNames),
TimeValue.parseTimeValue(request.param("timeout"), DEFAULT_TIMEOUT, getClass().getSimpleName() + ".timeout")
);
}

private boolean oneAndonlyOneIsSet(String deprecatedNodeDescription, String nodeIds, String nodeNames) {
if(Strings.hasText(deprecatedNodeDescription)) {
return Strings.isNullOrEmpty(nodeIds) && Strings.isNullOrEmpty(nodeNames);
}
else if (Strings.hasText(nodeIds)) {
return Strings.isNullOrEmpty(nodeNames);
}
else if (Strings.hasText(nodeNames)) {
return true;
}
else {
// none of the node identifiers are set
return false;
}

}

}