diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/SingleNodeShutdownMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/SingleNodeShutdownMetadata.java index 266b36c7c312d..93f82e29d9274 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/SingleNodeShutdownMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/SingleNodeShutdownMetadata.java @@ -8,6 +8,8 @@ package org.elasticsearch.cluster.metadata; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Version; import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.Diffable; import org.elasticsearch.common.io.stream.StreamInput; @@ -33,6 +35,8 @@ public class SingleNodeShutdownMetadata extends AbstractDiffable { + public static final Version REPLACE_SHUTDOWN_TYPE_ADDED_VERSION = Version.V_8_0_0; + public static final ParseField NODE_ID_FIELD = new ParseField("node_id"); public static final ParseField TYPE_FIELD = new ParseField("type"); public static final ParseField REASON_FIELD = new ParseField("reason"); @@ -40,6 +44,7 @@ public class SingleNodeShutdownMetadata extends AbstractDiffable PARSER = new ConstructingObjectParser<>( "node_shutdown_info", @@ -49,7 +54,8 @@ public class SingleNodeShutdownMetadata extends AbstractDiffable TimeValue.parseTimeValue(p.textOrNull(), ALLOCATION_DELAY_FIELD.getPreferredName()), ALLOCATION_DELAY_FIELD, ObjectParser.ValueType.STRING_OR_NULL ); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), TARGET_NODE_NAME_FIELD); } public static SingleNodeShutdownMetadata parse(XContentParser parser) { @@ -78,6 +85,7 @@ public static SingleNodeShutdownMetadata parse(XContentParser parser) { private final long startedAtMillis; private final boolean nodeSeen; @Nullable private final TimeValue allocationDelay; + @Nullable private final String targetNodeName; /** * @param nodeId The node ID that this shutdown metadata refers to. @@ -91,7 +99,8 @@ private SingleNodeShutdownMetadata( String reason, long startedAtMillis, boolean nodeSeen, - @Nullable TimeValue allocationDelay + @Nullable TimeValue allocationDelay, + @Nullable String targetNodeName ) { this.nodeId = Objects.requireNonNull(nodeId, "node ID must not be null"); this.type = Objects.requireNonNull(type, "shutdown type must not be null"); @@ -102,6 +111,13 @@ private SingleNodeShutdownMetadata( throw new IllegalArgumentException("shard allocation delay is only valid for RESTART-type shutdowns"); } this.allocationDelay = allocationDelay; + if (targetNodeName != null && type != Type.REPLACE) { + throw new IllegalArgumentException(new ParameterizedMessage("target node name is only valid for REPLACE type shutdowns, " + + "but was given type [{}] and target node name [{}]", type, targetNodeName).getFormattedMessage()); + } else if (targetNodeName == null && type == Type.REPLACE) { + throw new IllegalArgumentException("target node name is required for REPLACE type shutdowns"); + } + this.targetNodeName = targetNodeName; } public SingleNodeShutdownMetadata(StreamInput in) throws IOException { @@ -111,6 +127,11 @@ public SingleNodeShutdownMetadata(StreamInput in) throws IOException { this.startedAtMillis = in.readVLong(); this.nodeSeen = in.readBoolean(); this.allocationDelay = in.readOptionalTimeValue(); + if (in.getVersion().onOrAfter(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION)) { + this.targetNodeName = in.readOptionalString(); + } else { + this.targetNodeName = null; + } } /** @@ -148,6 +169,13 @@ public boolean getNodeSeen() { return nodeSeen; } + /** + * @return The name of the node to be used as a replacement for this node, or null. + */ + public String getTargetNodeName() { + return targetNodeName; + } + /** * @return The amount of time shard reallocation should be delayed for shards on this node, so that they will not be automatically * reassigned while the node is restarting. Will be {@code null} for non-restart shutdowns. @@ -165,11 +193,18 @@ public TimeValue getAllocationDelay() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(nodeId); - out.writeEnum(type); + if (out.getVersion().before(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION) && this.type == SingleNodeShutdownMetadata.Type.REPLACE) { + out.writeEnum(SingleNodeShutdownMetadata.Type.REMOVE); + } else { + out.writeEnum(type); + } out.writeString(reason); out.writeVLong(startedAtMillis); out.writeBoolean(nodeSeen); out.writeOptionalTimeValue(allocationDelay); + if (out.getVersion().onOrAfter(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION)) { + out.writeOptionalString(targetNodeName); + } } @Override @@ -184,6 +219,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (allocationDelay != null) { builder.field(ALLOCATION_DELAY_FIELD.getPreferredName(), allocationDelay.getStringRep()); } + if (targetNodeName != null) { + builder.field(TARGET_NODE_NAME_FIELD.getPreferredName(), targetNodeName); + } } builder.endObject(); @@ -200,7 +238,8 @@ && getNodeId().equals(that.getNodeId()) && getType() == that.getType() && getReason().equals(that.getReason()) && getNodeSeen() == that.getNodeSeen() - && Objects.equals(allocationDelay, that.allocationDelay); + && Objects.equals(allocationDelay, that.allocationDelay) + && Objects.equals(targetNodeName, that.targetNodeName); } @Override @@ -211,7 +250,8 @@ public int hashCode() { getReason(), getStartedAtMillis(), getNodeSeen(), - allocationDelay + allocationDelay, + targetNodeName ); } @@ -228,7 +268,8 @@ public static Builder builder(SingleNodeShutdownMetadata original) { .setType(original.getType()) .setReason(original.getReason()) .setStartedAtMillis(original.getStartedAtMillis()) - .setNodeSeen(original.getNodeSeen()); + .setNodeSeen(original.getNodeSeen()) + .setTargetNodeName(original.getTargetNodeName()); } public static class Builder { @@ -238,6 +279,7 @@ public static class Builder { private long startedAtMillis = -1; private boolean nodeSeen = false; private TimeValue allocationDelay; + private String targetNodeName; private Builder() {} @@ -295,6 +337,15 @@ public Builder setAllocationDelay(TimeValue allocationDelay) { return this; } + /** + * @param targetNodeName The name of the node which should be used to replcae this one. Only valid if the shutdown type is REPLACE. + * @return This builder. + */ + public Builder setTargetNodeName(String targetNodeName) { + this.targetNodeName = targetNodeName; + return this; + } + public SingleNodeShutdownMetadata build() { if (startedAtMillis == -1) { throw new IllegalArgumentException("start timestamp must be set"); @@ -306,7 +357,8 @@ public SingleNodeShutdownMetadata build() { reason, startedAtMillis, nodeSeen, - allocationDelay + allocationDelay, + targetNodeName ); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java index 0b22403198512..8dafeaa8ee429 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java @@ -87,6 +87,8 @@ private SingleNodeShutdownMetadata randomNodeShutdownInfo() { .setStartedAtMillis(randomNonNegativeLong()); if (type.equals(SingleNodeShutdownMetadata.Type.RESTART) && randomBoolean()) { builder.setAllocationDelay(TimeValue.parseTimeValue(randomTimeValue(), this.getTestName())); + } else if (type.equals(SingleNodeShutdownMetadata.Type.REPLACE)) { + builder.setTargetNodeName(randomAlphaOfLengthBetween(5,10)); } return builder.setNodeSeen(randomBoolean()) .build(); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/UnassignedInfoTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/UnassignedInfoTests.java index 21c1e8b3484fd..1857ae7689e3b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/UnassignedInfoTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/UnassignedInfoTests.java @@ -350,11 +350,14 @@ public void testRemainingDelayCalculationsWithUnrelatedShutdowns() throws Except Map shutdowns = new HashMap<>(); int numberOfShutdowns = randomIntBetween(1,15); for (int i = 0; i <= numberOfShutdowns; i++) { + final SingleNodeShutdownMetadata.Type type = randomFrom(EnumSet.allOf(SingleNodeShutdownMetadata.Type.class)); + final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; SingleNodeShutdownMetadata shutdown = SingleNodeShutdownMetadata.builder() .setNodeId(randomValueOtherThan(lastNodeId, () -> randomAlphaOfLengthBetween(5,10))) .setReason(this.getTestName()) .setStartedAtMillis(randomNonNegativeLong()) - .setType(randomFrom(EnumSet.allOf(SingleNodeShutdownMetadata.Type.class))) + .setType(type) + .setTargetNodeName(targetNodeName) .build(); shutdowns.put(shutdown.getNodeId(), shutdown); } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/NodeShutdownAllocationDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/NodeShutdownAllocationDeciderTests.java index 4112caaca3ece..0b224c690d0b1 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/NodeShutdownAllocationDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/NodeShutdownAllocationDeciderTests.java @@ -35,7 +35,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -176,13 +175,13 @@ public void testCannotAutoExpandToRemovingNode() { } private ClusterState prepareState(ClusterState initialState, SingleNodeShutdownMetadata.Type shutdownType) { - Map nodesShutdownInfo = new HashMap<>(); - + final String targetNodeName = shutdownType == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; final SingleNodeShutdownMetadata nodeShutdownMetadata = SingleNodeShutdownMetadata.builder() .setNodeId(DATA_NODE.getId()) .setType(shutdownType) .setReason(this.getTestName()) .setStartedAtMillis(1L) + .setTargetNodeName(targetNodeName) .build(); NodesShutdownMetadata nodesShutdownMetadata = new NodesShutdownMetadata(new HashMap<>()).putSingleNodeMetadata( nodeShutdownMetadata diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java index b45fef8b082cb..041f8ef3dfbb0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java @@ -356,15 +356,21 @@ public void testStepCompletableIfAllShardsActive() { ImmutableOpenMap.Builder indices = ImmutableOpenMap. builder().fPut(index.getName(), indexMetadata); + final SingleNodeShutdownMetadata.Type type = randomFrom( + SingleNodeShutdownMetadata.Type.REMOVE, + SingleNodeShutdownMetadata.Type.REPLACE + ); + final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE) .metadata(Metadata.builder() .indices(indices.build()) .putCustom(NodesShutdownMetadata.TYPE, new NodesShutdownMetadata(Collections.singletonMap("node1", SingleNodeShutdownMetadata.builder() - .setType(randomFrom(SingleNodeShutdownMetadata.Type.REMOVE, SingleNodeShutdownMetadata.Type.REPLACE)) + .setType(type) .setStartedAtMillis(randomNonNegativeLong()) .setReason("test") .setNodeId("node1") + .setTargetNodeName(targetNodeName) .build())))) .nodes(DiscoveryNodes.builder() .add(DiscoveryNode.createLocal(Settings.builder().put(node1Settings.build()) @@ -407,15 +413,21 @@ public void testStepBecomesUncompletable() { ImmutableOpenMap.Builder indices = ImmutableOpenMap. builder().fPut(index.getName(), indexMetadata); + final SingleNodeShutdownMetadata.Type type = randomFrom( + SingleNodeShutdownMetadata.Type.REMOVE, + SingleNodeShutdownMetadata.Type.REPLACE + ); + final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; ClusterState clusterState = ClusterState.builder(ClusterState.EMPTY_STATE) .metadata(Metadata.builder() .indices(indices.build()) .putCustom(NodesShutdownMetadata.TYPE, new NodesShutdownMetadata(Collections.singletonMap("node1", SingleNodeShutdownMetadata.builder() - .setType(randomFrom(SingleNodeShutdownMetadata.Type.REMOVE, SingleNodeShutdownMetadata.Type.REPLACE)) + .setType(type) .setStartedAtMillis(randomNonNegativeLong()) .setReason("test") .setNodeId("node1") + .setTargetNodeName(targetNodeName) .build())))) .nodes(DiscoveryNodes.builder() .add(DiscoveryNode.createLocal(Settings.builder().put(node1Settings.build()) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java index 7a5efada07fae..e0a1e2a36e939 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java @@ -580,6 +580,11 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Collections.emptySet())); + final SingleNodeShutdownMetadata.Type type = randomFrom( + SingleNodeShutdownMetadata.Type.REMOVE, + SingleNodeShutdownMetadata.Type.REPLACE + ); + final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; state = ClusterState.builder(state) .metadata(Metadata.builder(state.metadata()) .putCustom(NodesShutdownMetadata.TYPE, new NodesShutdownMetadata(Collections.singletonMap("shutdown_node", @@ -587,7 +592,8 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { .setNodeId("shutdown_node") .setReason("shut down for test") .setStartedAtMillis(randomNonNegativeLong()) - .setType(randomFrom(SingleNodeShutdownMetadata.Type.REMOVE, SingleNodeShutdownMetadata.Type.REPLACE)) + .setType(type) + .setTargetNodeName(targetNodeName) .build()))) .build()) .build(); diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlNodeShutdownIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlNodeShutdownIT.java index 798cdaaae785b..53af09dc7194f 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlNodeShutdownIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlNodeShutdownIT.java @@ -80,8 +80,8 @@ public void testJobsVacateShuttingDownNode() throws Exception { nodeIdToShutdown.get(), randomFrom(SingleNodeShutdownMetadata.Type.values()), "just testing", - null - ) + null, + null) ).actionGet(); // Wait for the desired end state of all 6 jobs running on nodes that are not shutting down. @@ -150,8 +150,8 @@ public void testCloseJobVacatingShuttingDownNode() throws Exception { nodeIdToShutdown.get(), randomFrom(SingleNodeShutdownMetadata.Type.values()), "just testing", - null - ) + null, + null) ) .actionGet(); diff --git a/x-pack/plugin/shutdown/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownIT.java b/x-pack/plugin/shutdown/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownIT.java index 9e4ff942f4a83..dbec7d358a9b7 100644 --- a/x-pack/plugin/shutdown/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownIT.java +++ b/x-pack/plugin/shutdown/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownIT.java @@ -10,10 +10,13 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.ObjectPath; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.core.Nullable; import org.elasticsearch.test.rest.ESRestTestCase; @@ -37,21 +40,25 @@ public class NodeShutdownIT extends ESRestTestCase { public void testRestartCRUD() throws Exception { - checkCRUD(randomFrom("restart", "RESTART"), randomPositiveTimeValue()); + checkCRUD(randomFrom("restart", "RESTART"), randomPositiveTimeValue(), null); } public void testRemoveCRUD() throws Exception { - checkCRUD(randomFrom("remove", "REMOVE"), null); + checkCRUD(randomFrom("remove", "REMOVE"), null, null); + } + + public void testReplaceCRUD() throws Exception { + checkCRUD(randomFrom("replace", "REPLACE"), null, randomAlphaOfLength(10)); } @SuppressWarnings("unchecked") - public void checkCRUD(String type, String allocationDelay) throws Exception { + public void checkCRUD(String type, @Nullable String allocationDelay, @Nullable String targetNodeName) throws Exception { String nodeIdToShutdown = getRandomNodeId(); // Ensure if we do a GET before the cluster metadata is set up, we don't get an error assertNoShuttingDownNodes(nodeIdToShutdown); - putNodeShutdown(nodeIdToShutdown, type, allocationDelay); + putNodeShutdown(nodeIdToShutdown, type, allocationDelay, targetNodeName); // Ensure we can read it back { @@ -63,6 +70,7 @@ public void checkCRUD(String type, String allocationDelay) throws Exception { assertThat((String) nodesArray.get(0).get("type"), equalToIgnoringCase(type)); assertThat(nodesArray.get(0).get("reason"), equalTo(this.getTestName())); assertThat(nodesArray.get(0).get("allocation_delay"), equalTo(allocationDelay)); + assertThat(nodesArray.get(0).get("target_node_name"), equalTo(targetNodeName)); } // Delete it and make sure it's deleted @@ -364,22 +372,71 @@ private void assertUnassignedShard(String nodeIdToShutdown, String indexName) th } private void putNodeShutdown(String nodeIdToShutdown, String type) throws IOException { - putNodeShutdown(nodeIdToShutdown, type, null); + putNodeShutdown(nodeIdToShutdown, type, null, null); } - private void putNodeShutdown(String nodeIdToShutdown, String type, @Nullable String allocationDelay) throws IOException { + private void putNodeShutdown(String nodeIdToShutdown, String type, @Nullable String allocationDelay, @Nullable String targetNodeName) + throws IOException { String reason = this.getTestName(); // Put a shutdown request Request putShutdown = new Request("PUT", "_nodes/" + nodeIdToShutdown + "/shutdown"); - if (type.equalsIgnoreCase("restart") && allocationDelay != null) { - putShutdown.setJsonEntity( - "{\"type\": \"" + type + "\", \"reason\": \"" + reason + "\", \"allocation_delay\": \"" + allocationDelay + "\"}" - ); + try (XContentBuilder putBody = JsonXContent.contentBuilder()) { + putBody.startObject(); + { + putBody.field("type", type); + putBody.field("reason", reason); + if (allocationDelay != null) { + assertThat("allocation delay parameter is only valid for RESTART-type shutdowns", type, equalToIgnoringCase("restart")); + putBody.field("allocation_delay", allocationDelay); + } + if (targetNodeName != null) { + assertThat( + "target node name parameter is only valid for REPLACE-type shutdowns", + type, + equalToIgnoringCase("replace") + ); + putBody.field("target_node_name", targetNodeName); + } else { + assertThat("target node name is required for REPALCE-type shutdowns", type, not(equalToIgnoringCase("replace"))); + } + } + putBody.endObject(); + putShutdown.setJsonEntity(Strings.toString(putBody)); + } + + if (type.equalsIgnoreCase("restart") && allocationDelay != null) { + assertNull("target node name parameter is only valid for REPLACE-type shutdowns", targetNodeName); + try (XContentBuilder putBody = JsonXContent.contentBuilder()) { + putBody.startObject(); + { + putBody.field("type", type); + putBody.field("reason", reason); + putBody.field("allocation_delay", allocationDelay); + } + putBody.endObject(); + putShutdown.setJsonEntity(Strings.toString(putBody)); + } } else { assertNull("allocation delay parameter is only valid for RESTART-type shutdowns", allocationDelay); - putShutdown.setJsonEntity("{\"type\": \"" + type + "\", \"reason\": \"" + reason + "\"}"); + try (XContentBuilder putBody = JsonXContent.contentBuilder()) { + putBody.startObject(); + { + putBody.field("type", type); + putBody.field("reason", reason); + if (targetNodeName != null) { + assertThat( + "target node name parameter is only valid for REPLACE-type shutdowns", + type, + equalToIgnoringCase("replace") + ); + putBody.field("target_node_name", targetNodeName); + } + } + putBody.endObject(); + putShutdown.setJsonEntity(Strings.toString(putBody)); + } } assertOK(client().performRequest(putShutdown)); } diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java index c89c95d1cf332..8ea56e7152a48 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java @@ -55,7 +55,8 @@ public void testShardAllocationIsDelayedForRestartingNode() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.RESTART, this.getTestName(), - null // Make sure it works with the default - we'll check this override in other tests + null, // Make sure it works with the default - we'll check this override in other tests + null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); assertTrue(putShutdownResponse.isAcknowledged()); @@ -93,7 +94,8 @@ public void testShardAllocationWillProceedAfterTimeout() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.RESTART, this.getTestName(), - TimeValue.timeValueMillis(randomIntBetween(10, 1000)) + TimeValue.timeValueMillis(randomIntBetween(10, 1000)), + null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); assertTrue(putShutdownResponse.isAcknowledged()); @@ -124,7 +126,8 @@ public void testIndexLevelAllocationDelayWillBeUsedIfLongerThanShutdownDelay() t nodeToRestartId, SingleNodeShutdownMetadata.Type.RESTART, this.getTestName(), - TimeValue.timeValueMillis(0) // No delay for reallocating these shards, IF this timeout is used. + TimeValue.timeValueMillis(0), // No delay for reallocating these shards, IF this timeout is used. + null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); assertTrue(putShutdownResponse.isAcknowledged()); @@ -151,7 +154,8 @@ public void testShardAllocationTimeoutCanBeChanged() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.RESTART, this.getTestName(), - TimeValue.timeValueMillis(1) + TimeValue.timeValueMillis(1), + null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); assertTrue(putShutdownResponse.isAcknowledged()); @@ -197,7 +201,8 @@ private String setupLongTimeoutTestCase() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.RESTART, this.getTestName(), - TimeValue.timeValueHours(3) + TimeValue.timeValueHours(3), + null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); assertTrue(putShutdownResponse.isAcknowledged()); diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownPluginsIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownPluginsIT.java index 0522593e6cf9f..e0cd7c6480360 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownPluginsIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownPluginsIT.java @@ -75,7 +75,7 @@ public void testShutdownAwarePlugin() throws Exception { // Mark the node as shutting down client().execute( PutShutdownNodeAction.INSTANCE, - new PutShutdownNodeAction.Request(shutdownNode, SingleNodeShutdownMetadata.Type.REMOVE, "removal for testing", null) + new PutShutdownNodeAction.Request(shutdownNode, SingleNodeShutdownMetadata.Type.REMOVE, "removal for testing", null, null) ).get(); GetShutdownStatusAction.Response getResp = client().execute( diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownShardsIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownShardsIT.java index c28fb909c1fe5..a7f6bb3003641 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownShardsIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownShardsIT.java @@ -47,6 +47,7 @@ public void testShardStatusStaysCompleteAfterNodeLeaves() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.REMOVE, this.getTestName(), + null, null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); @@ -85,6 +86,7 @@ public Settings onNodeStopped(String nodeName) throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.REMOVE, "testShardStatusStaysCompleteAfterNodeLeavesIfRegisteredWhileNodeOffline", + null, null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); @@ -122,6 +124,7 @@ public void testShardStatusIsCompleteOnNonDataNodes() throws Exception { nodeToRestartId, SingleNodeShutdownMetadata.Type.REMOVE, this.getTestName(), + null, null ); AcknowledgedResponse putShutdownResponse = client().execute(PutShutdownNodeAction.INSTANCE, putShutdownRequest).get(); diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java index d5ebce9e0f5ca..c0c965029ebb9 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java @@ -109,7 +109,7 @@ public void testTasksAreNotAssignedToShuttingDownNode() throws Exception { // Mark the node as shutting down client().execute( PutShutdownNodeAction.INSTANCE, - new PutShutdownNodeAction.Request(shutdownNode, SingleNodeShutdownMetadata.Type.REMOVE, "removal for testing", null) + new PutShutdownNodeAction.Request(shutdownNode, SingleNodeShutdownMetadata.Type.REMOVE, "removal for testing", null, null) ).get(); // Tell the persistent task executor it can start allocating the task diff --git a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/PutShutdownNodeAction.java b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/PutShutdownNodeAction.java index b9637d0d1a664..7a12d77e0aed3 100644 --- a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/PutShutdownNodeAction.java +++ b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/PutShutdownNodeAction.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.shutdown; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionType; import org.elasticsearch.action.support.master.AcknowledgedRequest; @@ -23,6 +24,8 @@ import java.io.IOException; +import static org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata.REPLACE_SHUTDOWN_TYPE_ADDED_VERSION; + public class PutShutdownNodeAction extends ActionType { public static final PutShutdownNodeAction INSTANCE = new PutShutdownNodeAction(); @@ -39,10 +42,13 @@ public static class Request extends AcknowledgedRequest { private final String reason; @Nullable private final TimeValue allocationDelay; + @Nullable + private final String targetNodeName; private static final ParseField TYPE_FIELD = new ParseField("type"); private static final ParseField REASON_FIELD = new ParseField("reason"); private static final ParseField ALLOCATION_DELAY_FIELD = new ParseField("allocation_delay"); + private static final ParseField TARGET_NODE_FIELD = new ParseField("target_node_name"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "put_node_shutdown_request", @@ -51,7 +57,8 @@ public static class Request extends AcknowledgedRequest { nodeId, SingleNodeShutdownMetadata.Type.parse((String) a[0]), (String) a[1], - a[2] == null ? null : TimeValue.parseTimeValue((String) a[2], "put-shutdown-node-request-" + nodeId) + a[2] == null ? null : TimeValue.parseTimeValue((String) a[2], "put-shutdown-node-request-" + nodeId), + (String) a[3] ) ); @@ -59,17 +66,25 @@ public static class Request extends AcknowledgedRequest { PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD); PARSER.declareString(ConstructingObjectParser.constructorArg(), REASON_FIELD); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), ALLOCATION_DELAY_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), TARGET_NODE_FIELD); } public static Request parseRequest(String nodeId, XContentParser parser) { return PARSER.apply(parser, nodeId); } - public Request(String nodeId, SingleNodeShutdownMetadata.Type type, String reason, @Nullable TimeValue allocationDelay) { + public Request( + String nodeId, + SingleNodeShutdownMetadata.Type type, + String reason, + @Nullable TimeValue allocationDelay, + @Nullable String targetNodeName + ) { this.nodeId = nodeId; this.type = type; this.reason = reason; this.allocationDelay = allocationDelay; + this.targetNodeName = targetNodeName; } public Request(StreamInput in) throws IOException { @@ -77,14 +92,26 @@ public Request(StreamInput in) throws IOException { this.type = in.readEnum(SingleNodeShutdownMetadata.Type.class); this.reason = in.readString(); this.allocationDelay = in.readOptionalTimeValue(); + if (in.getVersion().onOrAfter(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION)) { + this.targetNodeName = in.readOptionalString(); + } else { + this.targetNodeName = null; + } } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(nodeId); - out.writeEnum(type); + if (out.getVersion().before(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION) && this.type == SingleNodeShutdownMetadata.Type.REPLACE) { + out.writeEnum(SingleNodeShutdownMetadata.Type.REMOVE); + } else { + out.writeEnum(type); + } out.writeString(reason); out.writeOptionalTimeValue(allocationDelay); + if (out.getVersion().onOrAfter(REPLACE_SHUTDOWN_TYPE_ADDED_VERSION)) { + out.writeOptionalString(targetNodeName); + } } public String getNodeId() { @@ -103,6 +130,10 @@ public TimeValue getAllocationDelay() { return allocationDelay; } + public String getTargetNodeName() { + return targetNodeName; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException arve = new ActionRequestValidationException(); @@ -123,6 +154,18 @@ public ActionRequestValidationException validate() { arve.addValidationError(ALLOCATION_DELAY_FIELD + " is only allowed for RESTART-type shutdown requests"); } + if (targetNodeName != null && type != SingleNodeShutdownMetadata.Type.REPLACE) { + arve.addValidationError( + new ParameterizedMessage( + "target node name is only valid for REPLACE type shutdowns, " + "but was given type [{}] and target node name [{}]", + type, + targetNodeName + ).getFormattedMessage() + ); + } else if (targetNodeName == null && type == SingleNodeShutdownMetadata.Type.REPLACE) { + arve.addValidationError("target node name is required for REPLACE type shutdowns"); + } + if (arve.validationErrors().isEmpty() == false) { return arve; } else { diff --git a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/SingleNodeShutdownStatus.java b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/SingleNodeShutdownStatus.java index a24911efeef95..8e9313743adbf 100644 --- a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/SingleNodeShutdownStatus.java +++ b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/SingleNodeShutdownStatus.java @@ -33,6 +33,7 @@ public class SingleNodeShutdownStatus implements Writeable, ToXContentObject { private static final ParseField SHARD_MIGRATION_FIELD = new ParseField("shard_migration"); private static final ParseField PERSISTENT_TASKS_FIELD = new ParseField("persistent_tasks"); private static final ParseField PLUGINS_STATUS = new ParseField("plugins"); + private static final ParseField TARGET_NODE_NAME_FIELD = new ParseField("target_node_name"); public SingleNodeShutdownStatus( SingleNodeShutdownMetadata metadata, @@ -128,6 +129,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(SHARD_MIGRATION_FIELD.getPreferredName(), shardMigrationStatus); builder.field(PERSISTENT_TASKS_FIELD.getPreferredName(), persistentTasksStatus); builder.field(PLUGINS_STATUS.getPreferredName(), pluginsStatus); + if (metadata.getTargetNodeName() != null) { + builder.field(TARGET_NODE_NAME_FIELD.getPreferredName(), metadata.getTargetNodeName()); + } } builder.endObject(); return builder; diff --git a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java index 493249afb29cc..6d95c0eac8779 100644 --- a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java +++ b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java @@ -91,6 +91,7 @@ public ClusterState execute(ClusterState currentState) { .setStartedAtMillis(System.currentTimeMillis()) .setNodeSeen(nodeSeen) .setAllocationDelay(request.getAllocationDelay()) + .setTargetNodeName(request.getTargetNodeName()) .build(); return ClusterState.builder(currentState) diff --git a/x-pack/plugin/shutdown/src/test/java/org/elasticsearch/xpack/shutdown/GetShutdownStatusResponseTests.java b/x-pack/plugin/shutdown/src/test/java/org/elasticsearch/xpack/shutdown/GetShutdownStatusResponseTests.java index 58ef961ad04d6..1195ab279e4fd 100644 --- a/x-pack/plugin/shutdown/src/test/java/org/elasticsearch/xpack/shutdown/GetShutdownStatusResponseTests.java +++ b/x-pack/plugin/shutdown/src/test/java/org/elasticsearch/xpack/shutdown/GetShutdownStatusResponseTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.ShutdownShardMigrationStatus; import org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.AbstractWireSerializingTestCase; import java.io.IOException; @@ -46,11 +47,18 @@ protected GetShutdownStatusAction.Response mutateInstance(GetShutdownStatusActio } public static SingleNodeShutdownMetadata randomNodeShutdownMetadata() { + final SingleNodeShutdownMetadata.Type type = randomFrom(EnumSet.allOf(SingleNodeShutdownMetadata.Type.class)); + final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; + final TimeValue allocationDelay = type == SingleNodeShutdownMetadata.Type.RESTART && randomBoolean() + ? TimeValue.parseTimeValue(randomPositiveTimeValue(), GetShutdownStatusResponseTests.class.getSimpleName()) + : null; return SingleNodeShutdownMetadata.builder() .setNodeId(randomAlphaOfLength(5)) - .setType(randomFrom(EnumSet.allOf(SingleNodeShutdownMetadata.Type.class))) + .setType(type) .setReason(randomAlphaOfLength(5)) .setStartedAtMillis(randomNonNegativeLong()) + .setTargetNodeName(targetNodeName) + .setAllocationDelay(allocationDelay) .build(); }