() {
+// @Override
+// public void handleResponse(ClearVotingConfigExclusionsResponse response) {
+// logger.info("successfully cleared voting config after decommissioning");
+// }
+//
+// @Override
+// public void handleException(TransportException exp) {
+// logger.debug(new ParameterizedMessage("failure in clearing voting config exclusion after decommissioning"), exp);
+// }
+//
+// @Override
+// public String executor() {
+// return ThreadPool.Names.SAME;
+// }
+//
+// @Override
+// public ClearVotingConfigExclusionsResponse read(StreamInput in) throws IOException {
+// return new ClearVotingConfigExclusionsResponse(in);
+// }
+// }
+// );
+// }
+
+// /**
+// * Registers new decommissioned attribute metadata in the cluster state with {@link DecommissionStatus#DECOMMISSION_INIT}
+// *
+// * This method can be only called on the cluster-manager node. It tries to create a new decommissioned attribute on the master
+// * and if it was successful it adds new decommissioned attribute to cluster metadata.
+// *
+// * This method ensures that request is performed only on eligible cluster manager node
+// *
+// * @param decommissionAttribute register decommission attribute in the metadata request
+// * @param listener register decommission listener
+// */
+// private void registerDecommissionAttribute(
+// final DecommissionAttribute decommissionAttribute,
+// final ActionListener listener
+// ) {
+// if (!transportService.getLocalNode().isClusterManagerNode()
+// || nodeHasDecommissionedAttribute(transportService.getLocalNode(), decommissionAttribute)) {
+// throw new NotClusterManagerException(
+// "node ["
+// + transportService.getLocalNode().toString()
+// + "] not eligible to execute decommission request. Will retry until timeout."
+// );
+// }
+// clusterService.submitStateUpdateTask(
+// "put_decommission [" + decommissionAttribute + "]",
+// new ClusterStateUpdateTask(Priority.URGENT) {
+// @Override
+// public ClusterState execute(ClusterState currentState) {
+// logger.info(
+// "registering decommission metadata for attribute [{}] with status as [{}]",
+// decommissionAttribute.toString(),
+// DecommissionStatus.DECOMMISSION_INIT
+// );
+// Metadata metadata = currentState.metadata();
+// Metadata.Builder mdBuilder = Metadata.builder(metadata);
+// DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
+// ensureNoAwarenessAttributeDecommissioned(decommissionAttributeMetadata, decommissionAttribute);
+// decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute);
+// mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata);
+// return ClusterState.builder(currentState).metadata(mdBuilder).build();
+// }
+//
+// @Override
+// public void onFailure(String source, Exception e) {
+// if (e instanceof DecommissionFailedException) {
+// logger.error(
+// () -> new ParameterizedMessage("failed to decommission attribute [{}]", decommissionAttribute.toString()),
+// e
+// );
+// listener.onFailure(e);
+// } else if (e instanceof NotClusterManagerException) {
+// logger.debug(
+// () -> new ParameterizedMessage(
+// "cluster-manager updated while executing request for decommission attribute [{}]",
+// decommissionAttribute.toString()
+// ),
+// e
+// );
+// } else {
+// logger.error(
+// () -> new ParameterizedMessage(
+// "failed to initiate decommissioning for attribute [{}]",
+// decommissionAttribute.toString()
+// ),
+// e
+// );
+// listener.onFailure(e);
+// }
+// }
+//
+// @Override
+// public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+// DecommissionAttributeMetadata decommissionAttributeMetadata = newState.metadata()
+// .custom(DecommissionAttributeMetadata.TYPE);
+// assert decommissionAttribute.equals(decommissionAttributeMetadata.decommissionAttribute());
+// assert DecommissionStatus.DECOMMISSION_INIT.equals(decommissionAttributeMetadata.status());
+// listener.onResponse(new ClusterStateUpdateResponse(true));
+// initiateGracefulDecommission();
+// }
+// }
+// );
+// }
+
+// private void initiateGracefulDecommission() {
+// // maybe create a supplier for status update listener?
+// ActionListener listener = new ActionListener<>() {
+// @Override
+// public void onResponse(ClusterStateUpdateResponse clusterStateUpdateResponse) {
+// logger.info(
+// "updated decommission status to [{}], weighing away awareness attribute for graceful shutdown",
+// DecommissionStatus.DECOMMISSION_IN_PROGRESS
+// );
+// failDecommissionedNodes(clusterService.state());
+// }
+//
+// @Override
+// public void onFailure(Exception e) {
+// logger.error(
+// () -> new ParameterizedMessage(
+// "failed to update decommission status to [{}], will not proceed with decommission",
+// DecommissionStatus.DECOMMISSION_IN_PROGRESS
+// ),
+// e
+// );
+// }
+// };
+// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_IN_PROGRESS, listener);
+// // TODO - code for graceful decommission
+// }
+
+// private void failDecommissionedNodes(ClusterState state) {
+// DecommissionAttributeMetadata decommissionAttributeMetadata = state.metadata().custom(DecommissionAttributeMetadata.TYPE);
+// assert decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS)
+// : "unexpected status encountered while decommissioning nodes";
+// DecommissionAttribute decommissionAttribute = decommissionAttributeMetadata.decommissionAttribute();
+//
+// ActionListener statusUpdateListener = new ActionListener<>() {
+// @Override
+// public void onResponse(ClusterStateUpdateResponse clusterStateUpdateResponse) {
+// logger.info("successfully updated decommission status");
+// }
+//
+// @Override
+// public void onFailure(Exception e) {
+// logger.error("failed to update the decommission status");
+// }
+// };
+//
+// // execute decommissioning
+// decommissionHelper.handleNodesDecommissionRequest(
+// nodesWithDecommissionAttribute(state, decommissionAttribute),
+// "nodes-decommissioned"
+// );
+//
+// final ClusterStateObserver observer = new ClusterStateObserver(
+// clusterService,
+// TimeValue.timeValueSeconds(30L), // should this be a setting?
+// logger,
+// threadPool.getThreadContext()
+// );
+//
+// final Predicate allDecommissionedNodesRemoved = clusterState -> {
+// List nodesWithDecommissionAttribute = nodesWithDecommissionAttribute(clusterState, decommissionAttribute);
+// return nodesWithDecommissionAttribute.size() == 0;
+// };
+//
+// observer.waitForNextChange(new ClusterStateObserver.Listener() {
+// @Override
+// public void onNewClusterState(ClusterState state) {
+// logger.info("successfully removed all decommissioned nodes from the cluster");
+// clearVotingConfigAfterSuccessfulDecommission();
+// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_SUCCESSFUL, statusUpdateListener);
+// }
+//
+// @Override
+// public void onClusterServiceClose() {
+// logger.debug("cluster service closed while waiting for removal of decommissioned nodes.");
+// }
+//
+// @Override
+// public void onTimeout(TimeValue timeout) {
+// logger.info("timed out while waiting for removal of decommissioned nodes");
+// clearVotingConfigAfterSuccessfulDecommission();
+// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_FAILED, statusUpdateListener);
+// }
+// }, allDecommissionedNodesRemoved);
+// }
+//
+// private void updateMetadataWithDecommissionStatus(
+// DecommissionStatus decommissionStatus,
+// ActionListener listener
+// ) {
+// clusterService.submitStateUpdateTask(decommissionStatus.status(), new ClusterStateUpdateTask(Priority.URGENT) {
+// @Override
+// public ClusterState execute(ClusterState currentState) throws Exception {
+// Metadata metadata = currentState.metadata();
+// DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
+// assert decommissionAttributeMetadata != null && decommissionAttributeMetadata.decommissionAttribute() != null
+// : "failed to update status for decommission. metadata doesn't exist or invalid";
+// assert assertIncrementalStatusOrFailed(decommissionAttributeMetadata.status(), decommissionStatus);
+// Metadata.Builder mdBuilder = Metadata.builder(metadata);
+// DecommissionAttributeMetadata newMetadata = decommissionAttributeMetadata.withUpdatedStatus(decommissionStatus);
+// mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, newMetadata);
+// return ClusterState.builder(currentState).metadata(mdBuilder).build();
+// }
+//
+// @Override
+// public void onFailure(String source, Exception e) {
+// logger.error(() -> new ParameterizedMessage("failed to mark status as [{}]", decommissionStatus.status()), e);
+// }
+//
+// @Override
+// public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+// listener.onResponse(new ClusterStateUpdateResponse(true));
+// }
+//
+// });
+// }
+//
+// private List nodesWithDecommissionAttribute(ClusterState clusterState, DecommissionAttribute decommissionAttribute) {
+// List nodesWithDecommissionAttribute = new ArrayList<>();
+// final Predicate shouldRemoveNodePredicate = discoveryNode -> nodeHasDecommissionedAttribute(
+// discoveryNode,
+// decommissionAttribute
+// );
+// Iterator nodesIter = clusterState.nodes().getNodes().valuesIt();
+// while (nodesIter.hasNext()) {
+// final DiscoveryNode node = nodesIter.next();
+// if (shouldRemoveNodePredicate.test(node)) {
+// nodesWithDecommissionAttribute.add(node);
+// }
+// }
+// return nodesWithDecommissionAttribute;
+// }
+//
+// private static boolean assertIncrementalStatusOrFailed(DecommissionStatus oldStatus, DecommissionStatus newStatus) {
+// if (oldStatus == null || newStatus.equals(DecommissionStatus.DECOMMISSION_FAILED)) return true;
+// else if (newStatus.equals(DecommissionStatus.DECOMMISSION_SUCCESSFUL)) {
+// return oldStatus.equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS);
+// } else if (newStatus.equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS)) {
+// return oldStatus.equals(DecommissionStatus.DECOMMISSION_INIT);
+// }
+// return true;
+// }
+//
+// private static boolean nodeHasDecommissionedAttribute(DiscoveryNode discoveryNode, DecommissionAttribute decommissionAttribute) {
+// return discoveryNode.getAttributes().get(decommissionAttribute.attributeName()).equals(decommissionAttribute.attributeValue());
+// }
+//
+ private static void validateAwarenessAttribute(final DecommissionAttribute decommissionAttribute, List awarenessAttributes) {
+ if (!awarenessAttributes.contains(decommissionAttribute.attributeName())) {
+ throw new DecommissionFailedException(decommissionAttribute, "invalid awareness attribute requested for decommissioning");
+ }
+ // TODO - should attribute value be part of force zone values? If yes, read setting and throw exception if not found
+ }
+//
+// private static void ensureNoAwarenessAttributeDecommissioned(
+// DecommissionAttributeMetadata decommissionAttributeMetadata,
+// DecommissionAttribute decommissionAttribute
+// ) {
+// // If the previous decommission request failed, we will allow the request to pass this check
+// if (decommissionAttributeMetadata != null
+// && !decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_FAILED)) {
+// throw new DecommissionFailedException(
+// decommissionAttribute,
+// "one awareness attribute already decommissioned, recommission before triggering another decommission"
+// );
+// }
+// }
+
+ public void registerRecommissionAttribute(
+ final DecommissionAttribute recommissionAttribute,
+ final ActionListener listener) {
+ clusterService.submitStateUpdateTask(
+ "delete_decommission [" + recommissionAttribute + "]",
+ new ClusterStateUpdateTask(Priority.URGENT) {
+ @Override
+ public ClusterState execute(ClusterState currentState) {
+ return addRecommissionAttributeToCluster(currentState, recommissionAttribute);
+ }
+
+ @Override
+ public void onFailure(String source, Exception e) {
+ logger.error(() -> new ParameterizedMessage(
+ "failed to mark status as DECOMMISSION_FAILED for decommission attribute [{}]",
+ recommissionAttribute.toString()), e);
+ listener.onFailure(e);
+ }
+
+ @Override
+ public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
+ // Once the cluster state is processed we can try to recommission nodes by setting the weights for the zone.
+ // TODO Set the weights for the recommissioning zone.
+ listener.onResponse(new ClusterStateUpdateResponse(true));
+ }
+ }
+ );
+ }
+
+ ClusterState addRecommissionAttributeToCluster(final ClusterState currentState, final DecommissionAttribute recommissionAttribute) {
+ logger.info("Delete decommission request for attribute [{}] received", recommissionAttribute.toString());
+ Metadata metadata = currentState.metadata();
+ Metadata.Builder mdBuilder = Metadata.builder(metadata);
+ DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
+
+ if (!isValidRecommission(decommissionAttributeMetadata, recommissionAttribute)) {
+ throw new DecommissionFailedException(recommissionAttribute, "Recommission only allowed for decommissioned zone");
+ }
+
+ decommissionAttributeMetadata = new DecommissionAttributeMetadata(recommissionAttribute, DecommissionStatus.RECOMMISSION_IN_PROGRESS);
+ mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata);
+ return ClusterState.builder(currentState).metadata(mdBuilder).build();
+ }
+
+ public boolean isValidRecommission(DecommissionAttributeMetadata decommissionAttributeMetadata, DecommissionAttribute recommissionAttribute) {
+ if (decommissionAttributeMetadata != null
+ && (decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_SUCCESSFUL)
+ || decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS))
+ && decommissionAttributeMetadata.decommissionAttribute().attributeValue().equals(recommissionAttribute.attributeValue())) {
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionStatus.java b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionStatus.java
new file mode 100644
index 0000000000000..5217df43d8da8
--- /dev/null
+++ b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionStatus.java
@@ -0,0 +1,80 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.cluster.decommission;
+
+/**
+ * An enumeration of the states during decommissioning and recommissioning.
+ */
+public enum DecommissionStatus {
+ /**
+ * Decommission process is initiated
+ */
+ DECOMMISSION_INIT("decommission_init"),
+ /**
+ * Decommission process has started, decommissioned nodes should be weighed away
+ */
+ DECOMMISSION_IN_PROGRESS("decommission_in_progress"),
+ /**
+ * Decommissioning awareness attribute completed
+ */
+ DECOMMISSION_SUCCESSFUL("decommission_successful"),
+ /**
+ * Decommission request failed
+ */
+ DECOMMISSION_FAILED("decommission_failed"),
+ /**
+ * Recommission request received, recommissioning process has started
+ */
+ RECOMMISSION_IN_PROGRESS("recommission_in_progress"),
+ /**
+ * Recommission request failed. No nodes should fail to join the cluster with decommission exception
+ */
+ RECOMMISSION_FAILED("recommission_failed");
+
+ private final String status;
+
+ DecommissionStatus(String status) {
+ this.status = status;
+ }
+
+ /**
+ * Returns status that represents the decommission state
+ *
+ * @return status
+ */
+ public String status() {
+ return status;
+ }
+
+ /**
+ * Generate decommission status from given string
+ *
+ * @param status status in string
+ * @return status
+ */
+ public static DecommissionStatus fromString(String status) {
+ if (status == null) {
+ throw new IllegalArgumentException("decommission status cannot be null");
+ }
+ if (status.equals(DECOMMISSION_INIT.status())) {
+ return DECOMMISSION_INIT;
+ } else if (status.equals(DECOMMISSION_IN_PROGRESS.status())) {
+ return DECOMMISSION_IN_PROGRESS;
+ } else if (status.equals(DECOMMISSION_SUCCESSFUL.status())) {
+ return DECOMMISSION_SUCCESSFUL;
+ } else if (status.equals(DECOMMISSION_FAILED.status())) {
+ return DECOMMISSION_FAILED;
+ } else if (status.equals(RECOMMISSION_IN_PROGRESS.status())) {
+ return RECOMMISSION_IN_PROGRESS;
+ } else if (status.equals(RECOMMISSION_FAILED.status())) {
+ return RECOMMISSION_FAILED;
+ }
+ throw new IllegalStateException("Decommission status [" + status + "] not recognized.");
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/opensearch/cluster/decommission/NodeDecommissionedException.java b/server/src/main/java/org/opensearch/cluster/decommission/NodeDecommissionedException.java
new file mode 100644
index 0000000000000..c61fa7788624b
--- /dev/null
+++ b/server/src/main/java/org/opensearch/cluster/decommission/NodeDecommissionedException.java
@@ -0,0 +1,31 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.cluster.decommission;
+
+import org.opensearch.OpenSearchException;
+import org.opensearch.common.io.stream.StreamInput;
+
+import java.io.IOException;
+
+/**
+ * This exception is thrown if the node is decommissioned by @{@link DecommissionService}
+ * and this nodes needs to be removed from the cluster
+ *
+ * @opensearch.internal
+ */
+public class NodeDecommissionedException extends OpenSearchException {
+
+ public NodeDecommissionedException(String msg, Object... args) {
+ super(msg, args);
+ }
+
+ public NodeDecommissionedException(StreamInput in) throws IOException {
+ super(in);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/opensearch/cluster/metadata/DecommissionAttributeMetadata.java b/server/src/main/java/org/opensearch/cluster/metadata/DecommissionAttributeMetadata.java
new file mode 100644
index 0000000000000..11442f1596dc4
--- /dev/null
+++ b/server/src/main/java/org/opensearch/cluster/metadata/DecommissionAttributeMetadata.java
@@ -0,0 +1,228 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.cluster.metadata;
+
+import org.opensearch.OpenSearchParseException;
+import org.opensearch.Version;
+import org.opensearch.cluster.AbstractNamedDiffable;
+import org.opensearch.cluster.NamedDiff;
+import org.opensearch.cluster.decommission.DecommissionAttribute;
+import org.opensearch.cluster.decommission.DecommissionStatus;
+import org.opensearch.cluster.metadata.Metadata.Custom;
+import org.opensearch.common.Strings;
+import org.opensearch.common.io.stream.StreamInput;
+import org.opensearch.common.io.stream.StreamOutput;
+import org.opensearch.common.xcontent.ToXContent;
+import org.opensearch.common.xcontent.XContentBuilder;
+import org.opensearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * Contains metadata about decommission attribute
+ *
+ * @opensearch.internal
+ */
+public class DecommissionAttributeMetadata extends AbstractNamedDiffable implements Custom {
+
+ public static final String TYPE = "decommissionedAttribute";
+
+ private final DecommissionAttribute decommissionAttribute;
+ private final DecommissionStatus status;
+ public static final String attributeType = "awareness";
+
+ /**
+ * Constructs new decommission attribute metadata with given status
+ *
+ * @param decommissionAttribute attribute details
+ * @param status current status of the attribute decommission
+ */
+ public DecommissionAttributeMetadata(DecommissionAttribute decommissionAttribute, DecommissionStatus status) {
+ this.decommissionAttribute = decommissionAttribute;
+ this.status = status;
+ }
+
+ /**
+ * Constructs new decommission attribute metadata with status as {@link DecommissionStatus#DECOMMISSION_INIT}
+ *
+ * @param decommissionAttribute attribute details
+ */
+ public DecommissionAttributeMetadata(DecommissionAttribute decommissionAttribute) {
+ this(decommissionAttribute, DecommissionStatus.DECOMMISSION_INIT);
+ }
+
+ /**
+ * Returns the current decommissioned attribute
+ *
+ * @return decommissioned attributes
+ */
+ public DecommissionAttribute decommissionAttribute() {
+ return this.decommissionAttribute;
+ }
+
+ /**
+ * Returns the current status of the attribute decommission
+ *
+ * @return attribute type
+ */
+ public DecommissionStatus status() {
+ return this.status;
+ }
+
+ /**
+ * Creates a new instance that has the given decommission attribute moved to the given @{@link DecommissionStatus}
+ * @param status status to be updated with
+ * @return new instance with updated status
+ */
+ public DecommissionAttributeMetadata withUpdatedStatus(DecommissionStatus status) {
+ return new DecommissionAttributeMetadata(decommissionAttribute(), status);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ DecommissionAttributeMetadata that = (DecommissionAttributeMetadata) o;
+
+ if (!status.equals(that.status)) return false;
+ return decommissionAttribute.equals(that.decommissionAttribute);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(attributeType, decommissionAttribute, status);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getWriteableName() {
+ return TYPE;
+ }
+
+ @Override
+ public Version getMinimalSupportedVersion() {
+ return Version.V_2_3_0;
+ }
+
+ public DecommissionAttributeMetadata(StreamInput in) throws IOException {
+ this.decommissionAttribute = new DecommissionAttribute(in);
+ this.status = DecommissionStatus.fromString(in.readString());
+ }
+
+ public static NamedDiff readDiffFrom(StreamInput in) throws IOException {
+ return readDiffFrom(Custom.class, TYPE, in);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ decommissionAttribute.writeTo(out);
+ out.writeString(status.status());
+ }
+
+ public static DecommissionAttributeMetadata fromXContent(XContentParser parser) throws IOException {
+ XContentParser.Token token;
+ DecommissionAttribute decommissionAttribute = null;
+ DecommissionStatus status = null;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ String currentFieldName = parser.currentName();
+ if (attributeType.equals(currentFieldName)) {
+ if (parser.nextToken() != XContentParser.Token.START_OBJECT) {
+ throw new OpenSearchParseException(
+ "failed to parse decommission attribute type [{}], expected object",
+ attributeType
+ );
+ }
+ token = parser.nextToken();
+ if (token != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ String fieldName = parser.currentName();
+ String value;
+ token = parser.nextToken();
+ if (token == XContentParser.Token.VALUE_STRING) {
+ value = parser.text();
+ } else {
+ throw new OpenSearchParseException(
+ "failed to parse attribute [{}], expected string for attribute value",
+ fieldName
+ );
+ }
+ decommissionAttribute = new DecommissionAttribute(fieldName, value);
+ token = parser.nextToken();
+ } else {
+ throw new OpenSearchParseException("failed to parse attribute type [{}], unexpected type", attributeType);
+ }
+ } else {
+ throw new OpenSearchParseException("failed to parse attribute type [{}]", attributeType);
+ }
+ } else if ("status".equals(currentFieldName)) {
+ if (parser.nextToken() != XContentParser.Token.VALUE_STRING) {
+ throw new OpenSearchParseException(
+ "failed to parse status of decommissioning, expected string but found unknown type"
+ );
+ }
+ status = DecommissionStatus.fromString(parser.text());
+ } else {
+ throw new OpenSearchParseException(
+ "unknown field found [{}], failed to parse the decommission attribute",
+ currentFieldName
+ );
+ }
+ }
+ }
+ return new DecommissionAttributeMetadata(decommissionAttribute, status);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+ toXContent(decommissionAttribute, status, attributeType, builder, params);
+ return builder;
+ }
+
+ @Override
+ public EnumSet context() {
+ return Metadata.API_AND_GATEWAY;
+ }
+
+ /**
+ * @param decommissionAttribute decommission attribute
+ * @param status decommission status
+ * @param attributeType attribute type
+ * @param builder XContent builder
+ * @param params serialization parameters
+ */
+ public static void toXContent(
+ DecommissionAttribute decommissionAttribute,
+ DecommissionStatus status,
+ String attributeType,
+ XContentBuilder builder,
+ ToXContent.Params params
+ ) throws IOException {
+ builder.startObject(attributeType);
+ builder.field(decommissionAttribute.attributeName(), decommissionAttribute.attributeValue());
+ builder.endObject();
+ builder.field("status", status.status());
+ }
+
+ @Override
+ public String toString() {
+ return Strings.toString(this);
+ }
+}
\ No newline at end of file
diff --git a/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java b/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java
new file mode 100644
index 0000000000000..17a78cdd1ee16
--- /dev/null
+++ b/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java
@@ -0,0 +1,64 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ */
+
+package org.opensearch.cluster.decommission;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.After;
+import org.opensearch.cluster.ClusterName;
+import org.opensearch.cluster.ClusterState;
+import org.opensearch.cluster.metadata.DecommissionAttributeMetadata;
+import org.opensearch.cluster.metadata.Metadata;
+import org.opensearch.cluster.service.ClusterService;
+import org.opensearch.common.settings.ClusterSettings;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.test.OpenSearchTestCase;
+import org.opensearch.threadpool.TestThreadPool;
+
+import static org.hamcrest.Matchers.*;
+
+public class DecommissionServiceTests extends OpenSearchTestCase {
+
+ private final TestThreadPool threadPool = new TestThreadPool(DecommissionServiceTests.class.getName());
+
+ @After
+ public void terminateThreadPool() {
+ terminate(threadPool);
+ }
+
+ public void testAddRecommissionAttributeToClusterWithWrongRecommission() {
+ final ClusterSettings settings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
+ DecommissionService service = new DecommissionService(new ClusterService(Settings.EMPTY, settings, threadPool));
+ DecommissionAttribute decommissionAttribute = new DecommissionAttribute("zone", "zone-2");
+ DecommissionAttributeMetadata decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute, DecommissionStatus.DECOMMISSION_SUCCESSFUL);
+ final DecommissionAttribute recommissionAttribute = new DecommissionAttribute("zone", "zone-3");
+ ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
+ .metadata(Metadata.builder()
+ .putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata).build())
+ .build();
+
+ DecommissionFailedException e = expectThrows(DecommissionFailedException.class, () -> service.addRecommissionAttributeToCluster(clusterState, recommissionAttribute));
+ MatcherAssert.assertThat(e.getMessage(), containsString("Recommission only allowed for decommissioned zone"));
+ }
+
+ public void testAddRecommissionAttributeToCluster() {
+ final ClusterSettings settings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
+ DecommissionService service = new DecommissionService(new ClusterService(Settings.EMPTY, settings, threadPool));
+ DecommissionAttribute decommissionAttribute = new DecommissionAttribute("zone", "zone-2");
+ DecommissionAttributeMetadata decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute, DecommissionStatus.DECOMMISSION_SUCCESSFUL);
+ final DecommissionAttribute recommissionAttribute = new DecommissionAttribute("zone", "zone-2");
+ ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
+ .metadata(Metadata.builder()
+ .putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata).build())
+ .build();
+
+ final ClusterState newClusterState = service.addRecommissionAttributeToCluster(clusterState, recommissionAttribute);
+ DecommissionAttributeMetadata metadata = newClusterState.metadata().custom(DecommissionAttributeMetadata.TYPE);
+ MatcherAssert.assertThat(metadata.status(), is(DecommissionStatus.RECOMMISSION_IN_PROGRESS));
+ }
+}
From 40d739be05cf05f940c9468d9f306eaf9c7bf27e Mon Sep 17 00:00:00 2001
From: pranikum <109206473+pranikum@users.noreply.github.com>
Date: Mon, 29 Aug 2022 19:51:35 +0530
Subject: [PATCH 02/42] Remove commented changes from service class
Signed-off-by: pranikum <109206473+pranikum@users.noreply.github.com>
---
.../decommission/DecommissionService.java | 362 +-----------------
1 file changed, 1 insertion(+), 361 deletions(-)
diff --git a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
index 58e40a0e0ffea..86b53c8da5947 100644
--- a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
+++ b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
@@ -67,373 +67,13 @@ public class DecommissionService {
private static final Logger logger = LogManager.getLogger(DecommissionService.class);
private final ClusterService clusterService;
-// private final TransportService transportService;
-// private final ThreadPool threadPool;
-// private final DecommissionHelper decommissionHelper;
private volatile List awarenessAttributes;
@Inject
- public DecommissionService(
- ClusterService clusterService) {
+ public DecommissionService(ClusterService clusterService) {
this.clusterService = clusterService;
-// this.transportService = transportService;
-// this.threadPool = threadPool;
-// this.decommissionHelper = new DecommissionHelper(clusterService, allocationService);
-// this.awarenessAttributes = AwarenessAllocationDecider.CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING.get(settings);
-// clusterSettings.addSettingsUpdateConsumer(
-// AwarenessAllocationDecider.CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING,
-// this::setAwarenessAttributes
-// );
}
-// List getAwarenessAttributes() {
-// return awarenessAttributes;
-// }
-//
-// private void setAwarenessAttributes(List awarenessAttributes) {
-// this.awarenessAttributes = awarenessAttributes;
-// }
-
-// public void initiateAttributeDecommissioning(
-// final DecommissionAttribute decommissionAttribute,
-// final ActionListener listener,
-// ClusterState state
-// ) {
-// validateAwarenessAttribute(decommissionAttribute, getAwarenessAttributes());
-// logger.info("initiating awareness attribute [{}] decommissioning", decommissionAttribute.toString());
-// excludeDecommissionedClusterManagerNodesFromVotingConfig(decommissionAttribute);
-// registerDecommissionAttribute(decommissionAttribute, listener);
-// }
-
-// private void excludeDecommissionedClusterManagerNodesFromVotingConfig(DecommissionAttribute decommissionAttribute) {
-// final Predicate shouldDecommissionPredicate = discoveryNode -> nodeHasDecommissionedAttribute(
-// discoveryNode,
-// decommissionAttribute
-// );
-// List clusterManagerNodesToBeDecommissioned = new ArrayList<>();
-// Iterator clusterManagerNodesIter = clusterService.state().nodes().getClusterManagerNodes().valuesIt();
-// while (clusterManagerNodesIter.hasNext()) {
-// final DiscoveryNode node = clusterManagerNodesIter.next();
-// if (shouldDecommissionPredicate.test(node)) {
-// clusterManagerNodesToBeDecommissioned.add(node.getName());
-// }
-// }
-// transportService.sendRequest(
-// transportService.getLocalNode(),
-// AddVotingConfigExclusionsAction.NAME,
-// new AddVotingConfigExclusionsRequest(clusterManagerNodesToBeDecommissioned.toArray(String[]::new)),
-// new TransportResponseHandler() {
-// @Override
-// public void handleResponse(AddVotingConfigExclusionsResponse response) {
-// logger.info(
-// "successfully removed decommissioned cluster manager eligible nodes [{}] from voting config, "
-// + "proceeding to drain the decommissioned nodes",
-// clusterManagerNodesToBeDecommissioned.toString()
-// );
-// }
-//
-// @Override
-// public void handleException(TransportException exp) {
-// logger.debug(
-// new ParameterizedMessage("failure in removing decommissioned cluster manager eligible nodes from voting config"),
-// exp
-// );
-// }
-//
-// @Override
-// public String executor() {
-// return ThreadPool.Names.SAME;
-// }
-//
-// @Override
-// public AddVotingConfigExclusionsResponse read(StreamInput in) throws IOException {
-// return new AddVotingConfigExclusionsResponse(in);
-// }
-// }
-// );
-// }
-//
-// private void clearVotingConfigAfterSuccessfulDecommission() {
-// final ClearVotingConfigExclusionsRequest clearVotingConfigExclusionsRequest = new ClearVotingConfigExclusionsRequest();
-// clearVotingConfigExclusionsRequest.setWaitForRemoval(true);
-// transportService.sendRequest(
-// transportService.getLocalNode(),
-// ClearVotingConfigExclusionsAction.NAME,
-// clearVotingConfigExclusionsRequest,
-// new TransportResponseHandler() {
-// @Override
-// public void handleResponse(ClearVotingConfigExclusionsResponse response) {
-// logger.info("successfully cleared voting config after decommissioning");
-// }
-//
-// @Override
-// public void handleException(TransportException exp) {
-// logger.debug(new ParameterizedMessage("failure in clearing voting config exclusion after decommissioning"), exp);
-// }
-//
-// @Override
-// public String executor() {
-// return ThreadPool.Names.SAME;
-// }
-//
-// @Override
-// public ClearVotingConfigExclusionsResponse read(StreamInput in) throws IOException {
-// return new ClearVotingConfigExclusionsResponse(in);
-// }
-// }
-// );
-// }
-
-// /**
-// * Registers new decommissioned attribute metadata in the cluster state with {@link DecommissionStatus#DECOMMISSION_INIT}
-// *
-// * This method can be only called on the cluster-manager node. It tries to create a new decommissioned attribute on the master
-// * and if it was successful it adds new decommissioned attribute to cluster metadata.
-// *
-// * This method ensures that request is performed only on eligible cluster manager node
-// *
-// * @param decommissionAttribute register decommission attribute in the metadata request
-// * @param listener register decommission listener
-// */
-// private void registerDecommissionAttribute(
-// final DecommissionAttribute decommissionAttribute,
-// final ActionListener listener
-// ) {
-// if (!transportService.getLocalNode().isClusterManagerNode()
-// || nodeHasDecommissionedAttribute(transportService.getLocalNode(), decommissionAttribute)) {
-// throw new NotClusterManagerException(
-// "node ["
-// + transportService.getLocalNode().toString()
-// + "] not eligible to execute decommission request. Will retry until timeout."
-// );
-// }
-// clusterService.submitStateUpdateTask(
-// "put_decommission [" + decommissionAttribute + "]",
-// new ClusterStateUpdateTask(Priority.URGENT) {
-// @Override
-// public ClusterState execute(ClusterState currentState) {
-// logger.info(
-// "registering decommission metadata for attribute [{}] with status as [{}]",
-// decommissionAttribute.toString(),
-// DecommissionStatus.DECOMMISSION_INIT
-// );
-// Metadata metadata = currentState.metadata();
-// Metadata.Builder mdBuilder = Metadata.builder(metadata);
-// DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
-// ensureNoAwarenessAttributeDecommissioned(decommissionAttributeMetadata, decommissionAttribute);
-// decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute);
-// mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata);
-// return ClusterState.builder(currentState).metadata(mdBuilder).build();
-// }
-//
-// @Override
-// public void onFailure(String source, Exception e) {
-// if (e instanceof DecommissionFailedException) {
-// logger.error(
-// () -> new ParameterizedMessage("failed to decommission attribute [{}]", decommissionAttribute.toString()),
-// e
-// );
-// listener.onFailure(e);
-// } else if (e instanceof NotClusterManagerException) {
-// logger.debug(
-// () -> new ParameterizedMessage(
-// "cluster-manager updated while executing request for decommission attribute [{}]",
-// decommissionAttribute.toString()
-// ),
-// e
-// );
-// } else {
-// logger.error(
-// () -> new ParameterizedMessage(
-// "failed to initiate decommissioning for attribute [{}]",
-// decommissionAttribute.toString()
-// ),
-// e
-// );
-// listener.onFailure(e);
-// }
-// }
-//
-// @Override
-// public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
-// DecommissionAttributeMetadata decommissionAttributeMetadata = newState.metadata()
-// .custom(DecommissionAttributeMetadata.TYPE);
-// assert decommissionAttribute.equals(decommissionAttributeMetadata.decommissionAttribute());
-// assert DecommissionStatus.DECOMMISSION_INIT.equals(decommissionAttributeMetadata.status());
-// listener.onResponse(new ClusterStateUpdateResponse(true));
-// initiateGracefulDecommission();
-// }
-// }
-// );
-// }
-
-// private void initiateGracefulDecommission() {
-// // maybe create a supplier for status update listener?
-// ActionListener listener = new ActionListener<>() {
-// @Override
-// public void onResponse(ClusterStateUpdateResponse clusterStateUpdateResponse) {
-// logger.info(
-// "updated decommission status to [{}], weighing away awareness attribute for graceful shutdown",
-// DecommissionStatus.DECOMMISSION_IN_PROGRESS
-// );
-// failDecommissionedNodes(clusterService.state());
-// }
-//
-// @Override
-// public void onFailure(Exception e) {
-// logger.error(
-// () -> new ParameterizedMessage(
-// "failed to update decommission status to [{}], will not proceed with decommission",
-// DecommissionStatus.DECOMMISSION_IN_PROGRESS
-// ),
-// e
-// );
-// }
-// };
-// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_IN_PROGRESS, listener);
-// // TODO - code for graceful decommission
-// }
-
-// private void failDecommissionedNodes(ClusterState state) {
-// DecommissionAttributeMetadata decommissionAttributeMetadata = state.metadata().custom(DecommissionAttributeMetadata.TYPE);
-// assert decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS)
-// : "unexpected status encountered while decommissioning nodes";
-// DecommissionAttribute decommissionAttribute = decommissionAttributeMetadata.decommissionAttribute();
-//
-// ActionListener statusUpdateListener = new ActionListener<>() {
-// @Override
-// public void onResponse(ClusterStateUpdateResponse clusterStateUpdateResponse) {
-// logger.info("successfully updated decommission status");
-// }
-//
-// @Override
-// public void onFailure(Exception e) {
-// logger.error("failed to update the decommission status");
-// }
-// };
-//
-// // execute decommissioning
-// decommissionHelper.handleNodesDecommissionRequest(
-// nodesWithDecommissionAttribute(state, decommissionAttribute),
-// "nodes-decommissioned"
-// );
-//
-// final ClusterStateObserver observer = new ClusterStateObserver(
-// clusterService,
-// TimeValue.timeValueSeconds(30L), // should this be a setting?
-// logger,
-// threadPool.getThreadContext()
-// );
-//
-// final Predicate allDecommissionedNodesRemoved = clusterState -> {
-// List nodesWithDecommissionAttribute = nodesWithDecommissionAttribute(clusterState, decommissionAttribute);
-// return nodesWithDecommissionAttribute.size() == 0;
-// };
-//
-// observer.waitForNextChange(new ClusterStateObserver.Listener() {
-// @Override
-// public void onNewClusterState(ClusterState state) {
-// logger.info("successfully removed all decommissioned nodes from the cluster");
-// clearVotingConfigAfterSuccessfulDecommission();
-// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_SUCCESSFUL, statusUpdateListener);
-// }
-//
-// @Override
-// public void onClusterServiceClose() {
-// logger.debug("cluster service closed while waiting for removal of decommissioned nodes.");
-// }
-//
-// @Override
-// public void onTimeout(TimeValue timeout) {
-// logger.info("timed out while waiting for removal of decommissioned nodes");
-// clearVotingConfigAfterSuccessfulDecommission();
-// updateMetadataWithDecommissionStatus(DecommissionStatus.DECOMMISSION_FAILED, statusUpdateListener);
-// }
-// }, allDecommissionedNodesRemoved);
-// }
-//
-// private void updateMetadataWithDecommissionStatus(
-// DecommissionStatus decommissionStatus,
-// ActionListener listener
-// ) {
-// clusterService.submitStateUpdateTask(decommissionStatus.status(), new ClusterStateUpdateTask(Priority.URGENT) {
-// @Override
-// public ClusterState execute(ClusterState currentState) throws Exception {
-// Metadata metadata = currentState.metadata();
-// DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
-// assert decommissionAttributeMetadata != null && decommissionAttributeMetadata.decommissionAttribute() != null
-// : "failed to update status for decommission. metadata doesn't exist or invalid";
-// assert assertIncrementalStatusOrFailed(decommissionAttributeMetadata.status(), decommissionStatus);
-// Metadata.Builder mdBuilder = Metadata.builder(metadata);
-// DecommissionAttributeMetadata newMetadata = decommissionAttributeMetadata.withUpdatedStatus(decommissionStatus);
-// mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, newMetadata);
-// return ClusterState.builder(currentState).metadata(mdBuilder).build();
-// }
-//
-// @Override
-// public void onFailure(String source, Exception e) {
-// logger.error(() -> new ParameterizedMessage("failed to mark status as [{}]", decommissionStatus.status()), e);
-// }
-//
-// @Override
-// public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
-// listener.onResponse(new ClusterStateUpdateResponse(true));
-// }
-//
-// });
-// }
-//
-// private List nodesWithDecommissionAttribute(ClusterState clusterState, DecommissionAttribute decommissionAttribute) {
-// List nodesWithDecommissionAttribute = new ArrayList<>();
-// final Predicate shouldRemoveNodePredicate = discoveryNode -> nodeHasDecommissionedAttribute(
-// discoveryNode,
-// decommissionAttribute
-// );
-// Iterator nodesIter = clusterState.nodes().getNodes().valuesIt();
-// while (nodesIter.hasNext()) {
-// final DiscoveryNode node = nodesIter.next();
-// if (shouldRemoveNodePredicate.test(node)) {
-// nodesWithDecommissionAttribute.add(node);
-// }
-// }
-// return nodesWithDecommissionAttribute;
-// }
-//
-// private static boolean assertIncrementalStatusOrFailed(DecommissionStatus oldStatus, DecommissionStatus newStatus) {
-// if (oldStatus == null || newStatus.equals(DecommissionStatus.DECOMMISSION_FAILED)) return true;
-// else if (newStatus.equals(DecommissionStatus.DECOMMISSION_SUCCESSFUL)) {
-// return oldStatus.equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS);
-// } else if (newStatus.equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS)) {
-// return oldStatus.equals(DecommissionStatus.DECOMMISSION_INIT);
-// }
-// return true;
-// }
-//
-// private static boolean nodeHasDecommissionedAttribute(DiscoveryNode discoveryNode, DecommissionAttribute decommissionAttribute) {
-// return discoveryNode.getAttributes().get(decommissionAttribute.attributeName()).equals(decommissionAttribute.attributeValue());
-// }
-//
- private static void validateAwarenessAttribute(final DecommissionAttribute decommissionAttribute, List awarenessAttributes) {
- if (!awarenessAttributes.contains(decommissionAttribute.attributeName())) {
- throw new DecommissionFailedException(decommissionAttribute, "invalid awareness attribute requested for decommissioning");
- }
- // TODO - should attribute value be part of force zone values? If yes, read setting and throw exception if not found
- }
-//
-// private static void ensureNoAwarenessAttributeDecommissioned(
-// DecommissionAttributeMetadata decommissionAttributeMetadata,
-// DecommissionAttribute decommissionAttribute
-// ) {
-// // If the previous decommission request failed, we will allow the request to pass this check
-// if (decommissionAttributeMetadata != null
-// && !decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_FAILED)) {
-// throw new DecommissionFailedException(
-// decommissionAttribute,
-// "one awareness attribute already decommissioned, recommission before triggering another decommission"
-// );
-// }
-// }
-
public void registerRecommissionAttribute(
final DecommissionAttribute recommissionAttribute,
final ActionListener listener) {
From 32d1499f80af4020409ca7f8ee5430574a555a30 Mon Sep 17 00:00:00 2001
From: pranikum <109206473+pranikum@users.noreply.github.com>
Date: Thu, 1 Sep 2022 19:50:33 +0530
Subject: [PATCH 03/42] Update Service layer to handle removal of recommission
zone attribute
Signed-off-by: pranikum <109206473+pranikum@users.noreply.github.com>
---
.../decommission/DecommissionService.java | 61 +++----------------
.../DecommissionServiceTests.java | 27 +++-----
2 files changed, 14 insertions(+), 74 deletions(-)
diff --git a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
index 86b53c8da5947..c9f5a05727483 100644
--- a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
+++ b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
@@ -12,39 +12,14 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.opensearch.action.ActionListener;
-import org.opensearch.action.admin.cluster.configuration.AddVotingConfigExclusionsAction;
-import org.opensearch.action.admin.cluster.configuration.AddVotingConfigExclusionsRequest;
-import org.opensearch.action.admin.cluster.configuration.AddVotingConfigExclusionsResponse;
-import org.opensearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsAction;
-import org.opensearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsRequest;
-import org.opensearch.action.admin.cluster.configuration.ClearVotingConfigExclusionsResponse;
import org.opensearch.cluster.ClusterState;
-import org.opensearch.cluster.ClusterStateObserver;
import org.opensearch.cluster.ClusterStateUpdateTask;
-import org.opensearch.cluster.NotClusterManagerException;
import org.opensearch.cluster.ack.ClusterStateUpdateResponse;
import org.opensearch.cluster.metadata.DecommissionAttributeMetadata;
import org.opensearch.cluster.metadata.Metadata;
-import org.opensearch.cluster.node.DiscoveryNode;
-import org.opensearch.cluster.routing.allocation.AllocationService;
-import org.opensearch.cluster.routing.allocation.decider.AwarenessAllocationDecider;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.Priority;
import org.opensearch.common.inject.Inject;
-import org.opensearch.common.io.stream.StreamInput;
-import org.opensearch.common.settings.ClusterSettings;
-import org.opensearch.common.settings.Settings;
-import org.opensearch.common.unit.TimeValue;
-import org.opensearch.threadpool.ThreadPool;
-import org.opensearch.transport.TransportException;
-import org.opensearch.transport.TransportResponseHandler;
-import org.opensearch.transport.TransportService;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.function.Predicate;
/**
* Service responsible for entire lifecycle of decommissioning and recommissioning an awareness attribute.
@@ -67,29 +42,24 @@ public class DecommissionService {
private static final Logger logger = LogManager.getLogger(DecommissionService.class);
private final ClusterService clusterService;
- private volatile List awarenessAttributes;
@Inject
public DecommissionService(ClusterService clusterService) {
this.clusterService = clusterService;
}
- public void registerRecommissionAttribute(
- final DecommissionAttribute recommissionAttribute,
- final ActionListener listener) {
+ public void clearDecommissionStatus(final ActionListener listener) {
clusterService.submitStateUpdateTask(
- "delete_decommission [" + recommissionAttribute + "]",
+ "delete_decommission",
new ClusterStateUpdateTask(Priority.URGENT) {
@Override
public ClusterState execute(ClusterState currentState) {
- return addRecommissionAttributeToCluster(currentState, recommissionAttribute);
+ return deleteDecommissionAttribute(currentState);
}
@Override
public void onFailure(String source, Exception e) {
- logger.error(() -> new ParameterizedMessage(
- "failed to mark status as DECOMMISSION_FAILED for decommission attribute [{}]",
- recommissionAttribute.toString()), e);
+ logger.error(() -> new ParameterizedMessage("Failed to clear decommission attribute."), e);
listener.onFailure(e);
}
@@ -103,28 +73,11 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS
);
}
- ClusterState addRecommissionAttributeToCluster(final ClusterState currentState, final DecommissionAttribute recommissionAttribute) {
- logger.info("Delete decommission request for attribute [{}] received", recommissionAttribute.toString());
+ ClusterState deleteDecommissionAttribute(final ClusterState currentState) {
+ logger.info("Delete decommission request received");
Metadata metadata = currentState.metadata();
Metadata.Builder mdBuilder = Metadata.builder(metadata);
- DecommissionAttributeMetadata decommissionAttributeMetadata = metadata.custom(DecommissionAttributeMetadata.TYPE);
-
- if (!isValidRecommission(decommissionAttributeMetadata, recommissionAttribute)) {
- throw new DecommissionFailedException(recommissionAttribute, "Recommission only allowed for decommissioned zone");
- }
-
- decommissionAttributeMetadata = new DecommissionAttributeMetadata(recommissionAttribute, DecommissionStatus.RECOMMISSION_IN_PROGRESS);
- mdBuilder.putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata);
+ mdBuilder.removeCustom(DecommissionAttributeMetadata.TYPE);
return ClusterState.builder(currentState).metadata(mdBuilder).build();
}
-
- public boolean isValidRecommission(DecommissionAttributeMetadata decommissionAttributeMetadata, DecommissionAttribute recommissionAttribute) {
- if (decommissionAttributeMetadata != null
- && (decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_SUCCESSFUL)
- || decommissionAttributeMetadata.status().equals(DecommissionStatus.DECOMMISSION_IN_PROGRESS))
- && decommissionAttributeMetadata.decommissionAttribute().attributeValue().equals(recommissionAttribute.attributeValue())) {
- return true;
- }
- return false;
- }
}
\ No newline at end of file
diff --git a/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java b/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java
index 17a78cdd1ee16..ed0239367685c 100644
--- a/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java
+++ b/server/src/test/java/org/opensearch/cluster/decommission/DecommissionServiceTests.java
@@ -31,34 +31,21 @@ public void terminateThreadPool() {
terminate(threadPool);
}
- public void testAddRecommissionAttributeToClusterWithWrongRecommission() {
+ public void testClearDecommissionAttribute() {
final ClusterSettings settings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
DecommissionService service = new DecommissionService(new ClusterService(Settings.EMPTY, settings, threadPool));
DecommissionAttribute decommissionAttribute = new DecommissionAttribute("zone", "zone-2");
- DecommissionAttributeMetadata decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute, DecommissionStatus.DECOMMISSION_SUCCESSFUL);
- final DecommissionAttribute recommissionAttribute = new DecommissionAttribute("zone", "zone-3");
- ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
- .metadata(Metadata.builder()
- .putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata).build())
- .build();
-
- DecommissionFailedException e = expectThrows(DecommissionFailedException.class, () -> service.addRecommissionAttributeToCluster(clusterState, recommissionAttribute));
- MatcherAssert.assertThat(e.getMessage(), containsString("Recommission only allowed for decommissioned zone"));
- }
-
- public void testAddRecommissionAttributeToCluster() {
- final ClusterSettings settings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
- DecommissionService service = new DecommissionService(new ClusterService(Settings.EMPTY, settings, threadPool));
- DecommissionAttribute decommissionAttribute = new DecommissionAttribute("zone", "zone-2");
- DecommissionAttributeMetadata decommissionAttributeMetadata = new DecommissionAttributeMetadata(decommissionAttribute, DecommissionStatus.DECOMMISSION_SUCCESSFUL);
- final DecommissionAttribute recommissionAttribute = new DecommissionAttribute("zone", "zone-2");
+ DecommissionAttributeMetadata decommissionAttributeMetadata =
+ new DecommissionAttributeMetadata(decommissionAttribute, DecommissionStatus.DECOMMISSION_SUCCESSFUL);
ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
.metadata(Metadata.builder()
.putCustom(DecommissionAttributeMetadata.TYPE, decommissionAttributeMetadata).build())
.build();
- final ClusterState newClusterState = service.addRecommissionAttributeToCluster(clusterState, recommissionAttribute);
+ final ClusterState newClusterState = service.deleteDecommissionAttribute(clusterState);
DecommissionAttributeMetadata metadata = newClusterState.metadata().custom(DecommissionAttributeMetadata.TYPE);
- MatcherAssert.assertThat(metadata.status(), is(DecommissionStatus.RECOMMISSION_IN_PROGRESS));
+
+ // Decommission Attribute should be removed.
+ assertNull(metadata);
}
}
From ace11fe5746aec890828ab529a75a44edc1704e1 Mon Sep 17 00:00:00 2001
From: pranikum <109206473+pranikum@users.noreply.github.com>
Date: Wed, 7 Sep 2022 07:23:08 +0530
Subject: [PATCH 04/42] Add change log and address comment.
Signed-off-by: pranikum <109206473+pranikum@users.noreply.github.com>
---
CHANGELOG.md | 1 +
.../opensearch/cluster/decommission/DecommissionService.java | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9082ed039712..ee6fa3336b0aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Support for HTTP/2 (server-side) ([#3847](https://github.com/opensearch-project/OpenSearch/pull/3847))
- BWC version 2.2.2 ([#4383](https://github.com/opensearch-project/OpenSearch/pull/4383))
- Support for labels on version bump PRs, skip label support for changelog verifier ([#4391](https://github.com/opensearch-project/OpenSearch/pull/4391))
+- Recommission API changes for service layer ([#4320](https://github.com/opensearch-project/OpenSearch/pull/4320))
### Dependencies
- Bumps `org.gradle.test-retry` from 1.4.0 to 1.4.1
diff --git a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
index c9f5a05727483..0463a5ebbdd8b 100644
--- a/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
+++ b/server/src/main/java/org/opensearch/cluster/decommission/DecommissionService.java
@@ -50,7 +50,7 @@ public DecommissionService(ClusterService clusterService) {
public void clearDecommissionStatus(final ActionListener listener) {
clusterService.submitStateUpdateTask(
- "delete_decommission",
+ "delete_decommission_state",
new ClusterStateUpdateTask(Priority.URGENT) {
@Override
public ClusterState execute(ClusterState currentState) {
From 3988e2d899251250a62ec674f5f79de913b6005d Mon Sep 17 00:00:00 2001
From: pranikum <109206473+pranikum@users.noreply.github.com>
Date: Wed, 7 Sep 2022 07:28:04 +0530
Subject: [PATCH 05/42] Fix EOL Errors
Signed-off-by: pranikum <109206473+pranikum@users.noreply.github.com>
---
OpenSearch/build.gradle | 19 ++
OpenSearch/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes
.../gradle/wrapper/gradle-wrapper.properties | 5 +
OpenSearch/gradlew | 234 ++++++++++++++++++
OpenSearch/gradlew.bat | 89 +++++++
OpenSearch/settings.gradle | 2 +
.../decommission/DecommissionAttribute.java | 2 +-
.../DecommissionFailedException.java | 2 +-
.../decommission/DecommissionService.java | 2 +-
.../decommission/DecommissionStatus.java | 2 +-
.../NodeDecommissionedException.java | 2 +-
.../DecommissionAttributeMetadata.java | 2 +-
12 files changed, 355 insertions(+), 6 deletions(-)
create mode 100644 OpenSearch/build.gradle
create mode 100644 OpenSearch/gradle/wrapper/gradle-wrapper.jar
create mode 100644 OpenSearch/gradle/wrapper/gradle-wrapper.properties
create mode 100755 OpenSearch/gradlew
create mode 100644 OpenSearch/gradlew.bat
create mode 100644 OpenSearch/settings.gradle
diff --git a/OpenSearch/build.gradle b/OpenSearch/build.gradle
new file mode 100644
index 0000000000000..3cae2d720fb5d
--- /dev/null
+++ b/OpenSearch/build.gradle
@@ -0,0 +1,19 @@
+plugins {
+ id 'java'
+}
+
+group 'org.example'
+version '1.0-SNAPSHOT'
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/OpenSearch/gradle/wrapper/gradle-wrapper.jar b/OpenSearch/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b
GIT binary patch
literal 59821
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