diff --git a/docs/reference/ilm/actions/ilm-shrink.asciidoc b/docs/reference/ilm/actions/ilm-shrink.asciidoc index 5f13ec9aace74..3492c47fcd301 100644 --- a/docs/reference/ilm/actions/ilm-shrink.asciidoc +++ b/docs/reference/ilm/actions/ilm-shrink.asciidoc @@ -4,41 +4,29 @@ Phases allowed: hot, warm. -Sets an index to <> -and shrinks it into a new index with fewer primary shards. -The name of the new index is of the form `shrink-`. -For example, if the name of the source index is _logs_, -the name of the shrunken index is _shrink-logs_. +Sets a source index to <> and shrinks it into +a new index with fewer primary shards. The name of the resulting index is +`shrink--`. This action corresponds to the +<>. -The shrink action allocates all primary shards of the index to one node so it -can call the <> to shrink the index. -After shrinking, it swaps aliases that point to the original index to the new shrunken index. +After the `shrink` action, any aliases that pointed to the source index point to +the new shrunken index. If {ilm-init} performs the `shrink` action on a backing +index for a data stream, the shrunken index replaces the source index in the +stream. You cannot perform the `shrink` action on a write index. -To use the `shrink` action in the `hot` phase, the `rollover` action *must* be present. -If no rollover action is configured, {ilm-init} will reject the policy. +To use the `shrink` action in the `hot` phase, the `rollover` action *must* be +present. If no rollover action is configured, {ilm-init} will reject the policy. [IMPORTANT] -If the shrink action is used on a <>, -policy execution waits until the leader index rolls over (or is -<>), -then converts the follower index into a regular index with the -<> action before performing the shrink operation. - -If the managed index is part of a <>, -the shrunken index replaces the original index in the data stream. - -[NOTE] -This action cannot be performed on a data stream's write index. Attempts to do -so will fail. To shrink the index, first -<> the data stream. This -creates a new write index. Because the index is no longer the stream's write -index, the action can resume shrinking it. -Using a policy that makes use of the <> action -in the hot phase will avoid this situation and the need for a manual rollover for future -managed indices. +If the shrink action is used on a <>, policy +execution waits until the leader index rolls over (or is <>), then converts the follower index into a regular +index with the <> action before performing the shrink +operation. [[ilm-shrink-options]] ==== Shrink options + `number_of_shards`:: (Optional, integer) Number of shards to shrink to. @@ -103,3 +91,27 @@ PUT _ilm/policy/my_policy } } -------------------------------------------------- + +[[ilm-shrink-shard-allocation]] +==== Shard allocation for shrink + +During a `shrink` action, {ilm-init} allocates the source index's primary shards +to one node. After shrinking the index, {ilm-init} reallocates the shrunken +index's shards to the appropriate nodes based on your allocation rules. + +These allocation steps can fail for several reasons, including: + +* A node is removed during the `shrink` action. +* No node has enough disk space to host the source index's shards. +* {es} cannot reallocate the shrunken index due to conflicting allocation rules. + +When one of the allocation steps fails, {ilm-init} waits for the period set in +<>, +which defaults to 12 hours. This threshold period lets the cluster resolve any +issues causing the allocation failure. + +If the threshold period passes and {ilm-init} has not yet shrunk the index, +{ilm-init} attempts to allocate the source index's primary shards to another +node. If {ilm-init} shrunk the index but could not reallocate the shrunken +index's shards during the threshold period, {ilm-init} deletes the shrunken +index and re-attempts the entire `shrink` action. diff --git a/docs/reference/settings/ilm-settings.asciidoc b/docs/reference/settings/ilm-settings.asciidoc index 6aa79df9ec1f1..cd46d9b0b7b77 100644 --- a/docs/reference/settings/ilm-settings.asciidoc +++ b/docs/reference/settings/ilm-settings.asciidoc @@ -59,6 +59,13 @@ An index that was rolled over would normally match the full format, for example `logs-2016.10.31-000002`). If the index name doesn't match the pattern, index creation fails. +[[index-lifecycle-step-wait-time-threshold]] +`index.lifecycle.step.wait_time_threshold`:: +(<>, <>) +Time to wait for the cluster to resolve allocation issues during an {ilm-init} +<> action. Must be greater than `1h` (1 hour). Defaults to +`12h` (12 hours). See <>. + `index.lifecycle.rollover_alias`:: (<>, string) The index alias to update when the index rolls over. Specify when using a diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStep.java index 23861fbb639a5..1c413ac1b8e3c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStep.java @@ -39,6 +39,11 @@ public class CheckShrinkReadyStep extends ClusterStateWaitStep { super(key, nextStepKey); } + @Override + public boolean isRetryable() { + return true; + } + @Override public Result isConditionMet(Index index, ClusterState clusterState) { IndexMetadata idxMeta = clusterState.metadata().index(index); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStep.java new file mode 100644 index 0000000000000..9a3edf019f039 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStep.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.index.IndexNotFoundException; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; + +/** + * Deletes the index identified by the shrink index name stored in the lifecycle state of the managed index (if any was generated) + */ +public class CleanupShrinkIndexStep extends AsyncRetryDuringSnapshotActionStep { + public static final String NAME = "cleanup-shrink-index"; + private static final Logger logger = LogManager.getLogger(CleanupShrinkIndexStep.class); + + public CleanupShrinkIndexStep(StepKey key, StepKey nextStepKey, Client client) { + super(key, nextStepKey, client); + } + + @Override + public boolean isRetryable() { + return true; + } + + @Override + void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentClusterState, Listener listener) { + final String shrunkenIndexSource = IndexMetadata.INDEX_RESIZE_SOURCE_NAME.get(indexMetadata.getSettings()); + if (Strings.isNullOrEmpty(shrunkenIndexSource) == false) { + // the current managed index is a shrunk index + if (currentClusterState.metadata().index(shrunkenIndexSource) == null) { + // if the source index does not exist, we'll skip deleting the + // (managed) shrunk index as that will cause data loss + String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()); + logger.warn("managed index [{}] as part of policy [{}] is a shrunk index and the source index [{}] does not exist " + + "anymore. will skip the [{}] step", indexMetadata.getIndex().getName(), policyName, shrunkenIndexSource, NAME); + listener.onResponse(true); + return; + } + } + + LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata); + final String shrinkIndexName = lifecycleState.getShrinkIndexName(); + // if the shrink index was not generated there is nothing to delete so we move on + if (Strings.hasText(shrinkIndexName) == false) { + listener.onResponse(true); + return; + } + getClient().admin().indices() + .delete(new DeleteIndexRequest(shrinkIndexName).masterNodeTimeout(getMasterTimeout(currentClusterState)), + new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse acknowledgedResponse) { + // even if not all nodes acked the delete request yet we can consider this operation as successful as + // we'll generate a new index name and attempt to shrink into the newly generated name + listener.onResponse(true); + } + + @Override + public void onFailure(Exception e) { + if (e instanceof IndexNotFoundException) { + // we can move on if the index was deleted in the meantime + listener.onResponse(true); + } else { + listener.onFailure(e); + } + } + }); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java new file mode 100644 index 0000000000000..0fee72e060556 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.Index; +import org.elasticsearch.xpack.core.ilm.step.info.SingleMessageFieldInfo; + +import java.time.Clock; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; + +/** + * This step wraps an {@link ClusterStateWaitStep} in order to be able to manipulate what the next step will be, depending on the result of + * the wrapped {@link ClusterStateWaitStep}. + *

+ * If the action response is complete, the {@link ClusterStateWaitUntilThresholdStep}'s nextStepKey will be the nextStepKey of the + * wrapped action. When the threshold level is surpassed, if the underlying step's condition was not met, the nextStepKey will be changed to + * the provided {@link #nextKeyOnThresholdBreach} and this step will stop waiting. + * + * Failures encountered whilst executing the wrapped action will be propagated directly. + */ +public class ClusterStateWaitUntilThresholdStep extends ClusterStateWaitStep { + + private static final Logger logger = LogManager.getLogger(ClusterStateWaitUntilThresholdStep.class); + + private final ClusterStateWaitStep stepToExecute; + private final StepKey nextKeyOnThresholdBreach; + private final AtomicBoolean thresholdPassed = new AtomicBoolean(false); + + public ClusterStateWaitUntilThresholdStep(ClusterStateWaitStep stepToExecute, StepKey nextKeyOnThresholdBreach) { + super(stepToExecute.getKey(), stepToExecute.getNextStepKey()); + this.stepToExecute = stepToExecute; + this.nextKeyOnThresholdBreach = nextKeyOnThresholdBreach; + } + + @Override + public boolean isRetryable() { + return true; + } + + @Override + public Result isConditionMet(Index index, ClusterState clusterState) { + IndexMetadata idxMeta = clusterState.metadata().index(index); + if (idxMeta == null) { + // Index must have been since deleted, ignore it + logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", + getKey().getAction(), index.getName()); + return new Result(false, null); + } + + Result stepResult = stepToExecute.isConditionMet(index, clusterState); + + if (stepResult.isComplete() == false) { + // checking the threshold after we execute the step to make sure we execute the wrapped step at least once (because time is a + // wonderful thing) + TimeValue retryThreshold = LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING.get(idxMeta.getSettings()); + LifecycleExecutionState lifecycleState = fromIndexMetadata(idxMeta); + if (waitedMoreThanThresholdLevel(retryThreshold, lifecycleState, Clock.systemUTC())) { + // we retried this step enough, next step will be the configured to {@code nextKeyOnThresholdBreach} + thresholdPassed.set(true); + + String message = String.format(Locale.ROOT, "[%s] lifecycle step, as part of [%s] action, for index [%s] executed for" + + " more than [%s]. Abandoning execution and moving to the next fallback step [%s]", + getKey().getName(), getKey().getAction(), idxMeta.getIndex().getName(), retryThreshold, + nextKeyOnThresholdBreach); + logger.debug(message); + + return new Result(true, new SingleMessageFieldInfo(message)); + } + } + + return stepResult; + } + + static boolean waitedMoreThanThresholdLevel(@Nullable TimeValue retryThreshold, LifecycleExecutionState lifecycleState, Clock clock) { + assert lifecycleState.getStepTime() != null : "lifecycle state [" + lifecycleState + "] does not have the step time set"; + if (retryThreshold != null) { + // return true if the threshold was surpassed and false otherwise + return (lifecycleState.getStepTime() + retryThreshold.millis()) < clock.millis(); + } + return false; + } + + @Override + public StepKey getNextStepKey() { + if (thresholdPassed.get()) { + return nextKeyOnThresholdBreach; + } else { + return super.getNextStepKey(); + } + } + + /** + * Represents the {@link ClusterStateWaitStep} that's wrapped by this branching step. + */ + ClusterStateWaitStep getStepToExecute() { + return stepToExecute; + } + + /** + * The step key to be reported as the {@link ClusterStateWaitUntilThresholdStep#getNextStepKey()} if the index configured a max wait + * time using {@link LifecycleSettings#LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING} and the threshold was passed. + */ + StepKey getNextKeyOnThreshold() { + return nextKeyOnThresholdBreach; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (super.equals(o) == false) { + return false; + } + ClusterStateWaitUntilThresholdStep that = (ClusterStateWaitUntilThresholdStep) o; + return super.equals(o) + && Objects.equals(stepToExecute, that.stepToExecute) + && Objects.equals(nextKeyOnThresholdBreach, that.nextKeyOnThresholdBreach); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), stepToExecute, nextKeyOnThresholdBreach); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java index 206eb95656542..a61b6eb51d0fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java @@ -15,6 +15,7 @@ import org.elasticsearch.index.Index; import java.util.Objects; +import java.util.function.BiFunction; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; @@ -30,17 +31,24 @@ public class CopyExecutionStateStep extends ClusterStateActionStep { private static final Logger logger = LogManager.getLogger(CopyExecutionStateStep.class); - private final String targetIndexPrefix; + private final BiFunction targetIndexNameSupplier; private final StepKey targetNextStepKey; - public CopyExecutionStateStep(StepKey key, StepKey nextStepKey, String targetIndexPrefix, StepKey targetNextStepKey) { + public CopyExecutionStateStep(StepKey key, StepKey nextStepKey, + BiFunction targetIndexNameSupplier, + StepKey targetNextStepKey) { super(key, nextStepKey); - this.targetIndexPrefix = targetIndexPrefix; + this.targetIndexNameSupplier = targetIndexNameSupplier; this.targetNextStepKey = targetNextStepKey; } - String getTargetIndexPrefix() { - return targetIndexPrefix; + @Override + public boolean isRetryable() { + return true; + } + + BiFunction getTargetIndexNameSupplier() { + return targetIndexNameSupplier; } StepKey getTargetNextStepKey() { @@ -55,10 +63,9 @@ public ClusterState performAction(Index index, ClusterState clusterState) { logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), index.getName()); return clusterState; } - // get source index - String indexName = indexMetadata.getIndex().getName(); // get target index - String targetIndexName = targetIndexPrefix + indexName; + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetadata); + String targetIndexName = targetIndexNameSupplier.apply(index.getName(), lifecycleState); IndexMetadata targetIndexMetadata = clusterState.metadata().index(targetIndexName); if (targetIndexMetadata == null) { @@ -71,7 +78,6 @@ public ClusterState performAction(Index index, ClusterState clusterState) { String phase = targetNextStepKey.getPhase(); String action = targetNextStepKey.getAction(); String step = targetNextStepKey.getName(); - LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetadata); long lifecycleDate = lifecycleState.getLifecycleDate(); LifecycleExecutionState.Builder relevantTargetCustomData = LifecycleExecutionState.builder(); @@ -82,6 +88,7 @@ public ClusterState performAction(Index index, ClusterState clusterState) { relevantTargetCustomData.setSnapshotRepository(lifecycleState.getSnapshotRepository()); relevantTargetCustomData.setSnapshotName(lifecycleState.getSnapshotName()); relevantTargetCustomData.setSnapshotIndexName(lifecycleState.getSnapshotIndexName()); + relevantTargetCustomData.setShrinkIndexName(lifecycleState.getShrinkIndexName()); Metadata.Builder newMetadata = Metadata.builder(clusterState.getMetadata()) .put(IndexMetadata.builder(targetIndexMetadata) @@ -98,16 +105,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - if (super.equals(o) == false) { - return false; - } CopyExecutionStateStep that = (CopyExecutionStateStep) o; - return Objects.equals(targetIndexPrefix, that.targetIndexPrefix) && + return super.equals(o) && Objects.equals(targetIndexNameSupplier, that.targetIndexNameSupplier) && Objects.equals(targetNextStepKey, that.targetNextStepKey); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), targetIndexPrefix, targetNextStepKey); + return Objects.hash(super.hashCode(), targetIndexNameSupplier, targetNextStepKey); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java index f3641af4f3c84..9f2d2afd4eab5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java @@ -38,7 +38,7 @@ public class GenerateSnapshotNameStep extends ClusterStateActionStep { public static final String NAME = "generate-snapshot-name"; - private static final Logger logger = LogManager.getLogger(CreateSnapshotStep.class); + private static final Logger logger = LogManager.getLogger(GenerateSnapshotNameStep.class); private static final IndexNameExpressionResolver.DateMathExpressionResolver DATE_MATH_RESOLVER = new IndexNameExpressionResolver.DateMathExpressionResolver(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStep.java new file mode 100644 index 0000000000000..91860d12a6f2b --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStep.java @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.index.Index; +import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.Builder; + +import java.util.Locale; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; + +/** + * Generates a unique index name prefixing the original index name with the configured + * prefix, concatenated with a random UUID. The generated index name will be stored in the lifecycle + * execution state in the field designated by the configured setter method {@link #lifecycleStateSetter} + *

+ * The generated name will be in the format {prefix-randomUUID-indexName} + */ +public class GenerateUniqueIndexNameStep extends ClusterStateActionStep { + private static final Logger logger = LogManager.getLogger(GenerateUniqueIndexNameStep.class); + + public static final String NAME = "generate-index-name"; + static final String ILLEGAL_INDEXNAME_CHARS_REGEX = "[/:\"*?<>|# ,\\\\]+"; + static final int MAX_GENERATED_UUID_LENGTH = 4; + + private final String prefix; + private final BiFunction lifecycleStateSetter; + + public GenerateUniqueIndexNameStep(StepKey key, StepKey nextStepKey, String prefix, + BiFunction lifecycleStateSetter) { + super(key, nextStepKey); + this.prefix = prefix; + this.lifecycleStateSetter = lifecycleStateSetter; + } + + @Override + public boolean isRetryable() { + return true; + } + + String prefix() { + return prefix; + } + + BiFunction lifecycleStateSetter() { + return lifecycleStateSetter; + } + + @Override + public ClusterState performAction(Index index, ClusterState clusterState) { + IndexMetadata indexMetadata = clusterState.metadata().index(index); + if (indexMetadata == null) { + // Index must have been since deleted, ignore it + logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), index.getName()); + return clusterState; + } + + ClusterState.Builder newClusterStateBuilder = ClusterState.builder(clusterState); + + LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata); + + Builder newCustomData = LifecycleExecutionState.builder(lifecycleState); + String policy = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME); + String generatedIndexName = generateValidIndexName(prefix, index.getName()); + ActionRequestValidationException validationException = validateGeneratedIndexName(generatedIndexName, clusterState); + if (validationException != null) { + logger.warn("unable to generate a valid index name as part of policy [{}] for index [{}] due to [{}]", + policy, index.getName(), validationException.getMessage()); + throw validationException; + } + lifecycleStateSetter.apply(generatedIndexName, newCustomData); + + IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(indexMetadata); + indexMetadataBuilder.putCustom(ILM_CUSTOM_METADATA_KEY, newCustomData.build().asMap()); + newClusterStateBuilder.metadata(Metadata.builder(clusterState.getMetadata()).put(indexMetadataBuilder)); + return newClusterStateBuilder.build(); + } + + @Nullable + static ActionRequestValidationException validateGeneratedIndexName(String generatedIndexName, ClusterState state) { + ActionRequestValidationException err = new ActionRequestValidationException(); + try { + MetadataCreateIndexService.validateIndexOrAliasName(generatedIndexName, InvalidIndexNameException::new); + } catch (InvalidIndexNameException e) { + err.addValidationError(e.getMessage()); + } + if (state.routingTable().hasIndex(generatedIndexName) || state.metadata().hasIndex(generatedIndexName)) { + err.addValidationError("the index name we generated [" + generatedIndexName + "] already exists"); + } + if (state.metadata().hasAlias(generatedIndexName)) { + err.addValidationError("the index name we generated [" + generatedIndexName + "] already exists as alias"); + } + + if (err.validationErrors().size() > 0) { + return err; + } else { + return null; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GenerateUniqueIndexNameStep that = (GenerateUniqueIndexNameStep) o; + return super.equals(o) && Objects.equals(prefix, that.prefix); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), prefix); + } + + /** + * This generates a valid unique index name by using the provided prefix, appended with a generated UUID, and the index name. + */ + static String generateValidIndexName(String prefix, String indexName) { + String randomUUID = generateValidIndexSuffix(UUIDs::randomBase64UUID); + randomUUID = randomUUID.substring(0, Math.min(randomUUID.length(), MAX_GENERATED_UUID_LENGTH)); + return prefix + randomUUID + "-" + indexName; + } + + static String generateValidIndexSuffix(Supplier randomGenerator) { + String randomSuffix = randomGenerator.get().toLowerCase(Locale.ROOT); + randomSuffix = randomSuffix.replaceAll(ILLEGAL_INDEXNAME_CHARS_REGEX, ""); + if (randomSuffix.length() == 0) { + throw new IllegalArgumentException("unable to generate random index name suffix"); + } + + return randomSuffix; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java index 05c5b7998fd57..d46b556554e84 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java @@ -48,6 +48,7 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl private static final ParseField PHASE_EXECUTION_INFO = new ParseField("phase_execution"); private static final ParseField AGE_FIELD = new ParseField("age"); private static final ParseField REPOSITORY_NAME = new ParseField("repository_name"); + private static final ParseField SHRINK_INDEX_NAME = new ParseField("shrink_index_name"); private static final ParseField SNAPSHOT_NAME = new ParseField("snapshot_name"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -68,6 +69,7 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl (Long) (a[10]), (String) a[16], (String) a[17], + (String) a[18], (BytesReference) a[11], (PhaseExecutionInfo) a[12] // a[13] == "age" @@ -96,6 +98,7 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), FAILED_STEP_RETRY_COUNT_FIELD); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), REPOSITORY_NAME); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), SNAPSHOT_NAME); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), SHRINK_INDEX_NAME); } private final String index; @@ -115,25 +118,28 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl private final Integer failedStepRetryCount; private final String repositoryName; private final String snapshotName; + private final String shrinkIndexName; public static IndexLifecycleExplainResponse newManagedIndexResponse(String index, String policyName, Long lifecycleDate, - String phase, String action, String step, String failedStep, Boolean isAutoRetryableError, Integer failedStepRetryCount, - Long phaseTime, Long actionTime, Long stepTime, String repositoryName, String snapshotName, BytesReference stepInfo, - PhaseExecutionInfo phaseExecutionInfo) { + String phase, String action, String step, String failedStep, + Boolean isAutoRetryableError, Integer failedStepRetryCount, + Long phaseTime, Long actionTime, Long stepTime, + String repositoryName, String snapshotName, String shrinkIndexName, + BytesReference stepInfo, PhaseExecutionInfo phaseExecutionInfo) { return new IndexLifecycleExplainResponse(index, true, policyName, lifecycleDate, phase, action, step, failedStep, - isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo, - phaseExecutionInfo); + isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, shrinkIndexName, + stepInfo, phaseExecutionInfo); } public static IndexLifecycleExplainResponse newUnmanagedIndexResponse(String index) { return new IndexLifecycleExplainResponse(index, false, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null); + null, null, null, null); } private IndexLifecycleExplainResponse(String index, boolean managedByILM, String policyName, Long lifecycleDate, String phase, String action, String step, String failedStep, Boolean isAutoRetryableError, Integer failedStepRetryCount, Long phaseTime, Long actionTime, Long stepTime, - String repositoryName, String snapshotName, BytesReference stepInfo, + String repositoryName, String snapshotName, String shrinkIndexName, BytesReference stepInfo, PhaseExecutionInfo phaseExecutionInfo) { if (managedByILM) { if (policyName == null) { @@ -171,6 +177,7 @@ private IndexLifecycleExplainResponse(String index, boolean managedByILM, String this.phaseExecutionInfo = phaseExecutionInfo; this.repositoryName = repositoryName; this.snapshotName = snapshotName; + this.shrinkIndexName = shrinkIndexName; } public IndexLifecycleExplainResponse(StreamInput in) throws IOException { @@ -202,6 +209,11 @@ public IndexLifecycleExplainResponse(StreamInput in) throws IOException { repositoryName = null; snapshotName = null; } + if (in.getVersion().onOrAfter(Version.V_7_13_0)) { + shrinkIndexName = in.readOptionalString(); + } else { + shrinkIndexName = null; + } } else { policyName = null; lifecycleDate = null; @@ -218,6 +230,7 @@ public IndexLifecycleExplainResponse(StreamInput in) throws IOException { phaseExecutionInfo = null; repositoryName = null; snapshotName = null; + shrinkIndexName = null; } } @@ -245,6 +258,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(repositoryName); out.writeOptionalString(snapshotName); } + if (out.getVersion().onOrAfter(Version.V_7_13_0)) { + out.writeOptionalString(shrinkIndexName); + } } } @@ -324,6 +340,10 @@ public String getSnapshotName() { return snapshotName; } + public String getShrinkIndexName() { + return shrinkIndexName; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -368,6 +388,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (snapshotName != null) { builder.field(SNAPSHOT_NAME.getPreferredName(), snapshotName); } + if (shrinkIndexName != null) { + builder.field(SHRINK_INDEX_NAME.getPreferredName(), shrinkIndexName); + } if (stepInfo != null && stepInfo.length() > 0) { builder.rawField(STEP_INFO_FIELD.getPreferredName(), stepInfo.streamInput(), XContentType.JSON); } @@ -382,7 +405,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public int hashCode() { return Objects.hash(index, managedByILM, policyName, lifecycleDate, phase, action, step, failedStep, isAutoRetryableError, - failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo, phaseExecutionInfo); + failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, shrinkIndexName, stepInfo, + phaseExecutionInfo); } @Override @@ -409,6 +433,7 @@ public boolean equals(Object obj) { Objects.equals(stepTime, other.stepTime) && Objects.equals(repositoryName, other.repositoryName) && Objects.equals(snapshotName, other.snapshotName) && + Objects.equals(shrinkIndexName, other.shrinkIndexName) && Objects.equals(stepInfo, other.stepInfo) && Objects.equals(phaseExecutionInfo, other.phaseExecutionInfo); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java index 829b379941def..f74f04b85230a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java @@ -40,6 +40,7 @@ public class LifecycleExecutionState { private static final String SNAPSHOT_NAME = "snapshot_name"; private static final String SNAPSHOT_REPOSITORY = "snapshot_repository"; private static final String SNAPSHOT_INDEX_NAME = "snapshot_index_name"; + private static final String SHRINK_INDEX_NAME ="shrink_index_name"; private static final String ROLLUP_INDEX_NAME = "rollup_index_name"; private final String phase; @@ -56,13 +57,14 @@ public class LifecycleExecutionState { private final Long stepTime; private final String snapshotName; private final String snapshotRepository; + private final String shrinkIndexName; private final String snapshotIndexName; private final String rollupIndexName; private LifecycleExecutionState(String phase, String action, String step, String failedStep, Boolean isAutoRetryableError, Integer failedStepRetryCount, String stepInfo, String phaseDefinition, Long lifecycleDate, Long phaseTime, Long actionTime, Long stepTime, String snapshotRepository, String snapshotName, - String snapshotIndexName, String rollupIndexName) { + String shrinkIndexName, String snapshotIndexName, String rollupIndexName) { this.phase = phase; this.action = action; this.step = step; @@ -77,6 +79,7 @@ private LifecycleExecutionState(String phase, String action, String step, String this.stepTime = stepTime; this.snapshotRepository = snapshotRepository; this.snapshotName = snapshotName; + this.shrinkIndexName = shrinkIndexName; this.snapshotIndexName = snapshotIndexName; this.rollupIndexName = rollupIndexName; } @@ -138,6 +141,7 @@ public static Builder builder(LifecycleExecutionState state) { .setActionTime(state.actionTime) .setSnapshotRepository(state.snapshotRepository) .setSnapshotName(state.snapshotName) + .setShrinkIndexName(state.shrinkIndexName) .setSnapshotIndexName(state.snapshotIndexName) .setRollupIndexName(state.rollupIndexName) .setStepTime(state.stepTime); @@ -175,6 +179,9 @@ static LifecycleExecutionState fromCustomMetadata(Map customData if (customData.containsKey(SNAPSHOT_NAME)) { builder.setSnapshotName(customData.get(SNAPSHOT_NAME)); } + if (customData.containsKey(SHRINK_INDEX_NAME)) { + builder.setShrinkIndexName(customData.get(SHRINK_INDEX_NAME)); + } if (customData.containsKey(INDEX_CREATION_DATE)) { try { builder.setIndexCreationDate(Long.parseLong(customData.get(INDEX_CREATION_DATE))); @@ -257,7 +264,7 @@ public Map asMap() { result.put(STEP_TIME, String.valueOf(stepTime)); } if (phaseDefinition != null) { - result.put(PHASE_DEFINITION, String.valueOf(phaseDefinition)); + result.put(PHASE_DEFINITION, phaseDefinition); } if (snapshotRepository != null) { result.put(SNAPSHOT_REPOSITORY, snapshotRepository); @@ -265,6 +272,9 @@ public Map asMap() { if (snapshotName != null) { result.put(SNAPSHOT_NAME, snapshotName); } + if (shrinkIndexName != null) { + result.put(SHRINK_INDEX_NAME, shrinkIndexName); + } if (snapshotIndexName != null) { result.put(SNAPSHOT_INDEX_NAME, snapshotIndexName); } @@ -330,6 +340,10 @@ public String getSnapshotRepository() { return snapshotRepository; } + public String getShrinkIndexName() { + return shrinkIndexName; + } + public String getSnapshotIndexName() { return snapshotIndexName; } @@ -387,6 +401,7 @@ public static class Builder { private Integer failedStepRetryCount; private String snapshotName; private String snapshotRepository; + private String shrinkIndexName; private String snapshotIndexName; private String rollupIndexName; @@ -460,6 +475,11 @@ public Builder setSnapshotName(String snapshotName) { return this; } + public Builder setShrinkIndexName(String shrinkIndexName) { + this.shrinkIndexName = shrinkIndexName; + return this; + } + public Builder setSnapshotIndexName(String snapshotIndexName) { this.snapshotIndexName = snapshotIndexName; return this; @@ -472,7 +492,7 @@ public Builder setRollupIndexName(String rollupIndexName) { public LifecycleExecutionState build() { return new LifecycleExecutionState(phase, action, step, failedStep, isAutoRetryableError, failedStepRetryCount, stepInfo, - phaseDefinition, indexCreationDate, phaseTime, actionTime, stepTime, snapshotRepository, snapshotName, + phaseDefinition, indexCreationDate, phaseTime, actionTime, stepTime, snapshotRepository, snapshotName, shrinkIndexName, snapshotIndexName, rollupIndexName); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java index 62090a69fa408..3783f87f6de33 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java @@ -11,6 +11,8 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.scheduler.CronSchedule; +import static org.elasticsearch.common.settings.Setting.timeSetting; + /** * Class encapsulating settings related to Index Lifecycle Management X-Pack Plugin */ @@ -20,6 +22,7 @@ public class LifecycleSettings { public static final String LIFECYCLE_INDEXING_COMPLETE = "index.lifecycle.indexing_complete"; public static final String LIFECYCLE_ORIGINATION_DATE = "index.lifecycle.origination_date"; public static final String LIFECYCLE_PARSE_ORIGINATION_DATE = "index.lifecycle.parse_origination_date"; + public static final String LIFECYCLE_STEP_WAIT_TIME_THRESHOLD = "index.lifecycle.step.wait_time_threshold"; public static final String LIFECYCLE_HISTORY_INDEX_ENABLED = "indices.lifecycle.history_index_enabled"; public static final String LIFECYCLE_STEP_MASTER_TIMEOUT = "indices.lifecycle.step.master_timeout"; @@ -47,6 +50,13 @@ public class LifecycleSettings { public static final Setting LIFECYCLE_STEP_MASTER_TIMEOUT_SETTING = Setting.positiveTimeSetting(LIFECYCLE_STEP_MASTER_TIMEOUT, TimeValue.timeValueSeconds(30), Setting.Property.Dynamic, Setting.Property.NodeScope); + // This setting configures how much time since step_time should ILM wait for a condition to be met. After the threshold wait time has + // elapsed ILM will likely stop waiting and go to the next step. + // Also see {@link org.elasticsearch.xpack.core.ilm.ClusterStateWaitUntilThresholdStep} + public static final Setting LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING = + timeSetting(LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, TimeValue.timeValueHours(12), TimeValue.timeValueHours(1), Setting.Property.Dynamic, + Setting.Property.IndexScope); + public static final Setting SLM_HISTORY_INDEX_ENABLED_SETTING = Setting.boolSetting(SLM_HISTORY_INDEX_ENABLED, true, Setting.Property.NodeScope); @@ -64,7 +74,7 @@ public class LifecycleSettings { SLM_RETENTION_SCHEDULE + "]", e); } }, Setting.Property.Dynamic, Setting.Property.NodeScope); - public static final Setting SLM_RETENTION_DURATION_SETTING = Setting.timeSetting(SLM_RETENTION_DURATION, + public static final Setting SLM_RETENTION_DURATION_SETTING = timeSetting(SLM_RETENTION_DURATION, TimeValue.timeValueHours(1), TimeValue.timeValueMillis(500), Setting.Property.Dynamic, Setting.Property.NodeScope); public static final Setting SLM_MINIMUM_INTERVAL_SETTING = Setting.positiveTimeSetting(SLM_MINIMUM_INTERVAL, TimeValue.timeValueMinutes(15), Setting.Property.Dynamic, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStep.java index 9c227c0d29049..c1fc7f85ba65b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStep.java @@ -16,6 +16,9 @@ import java.util.Locale; import java.util.Objects; +import java.util.function.BiFunction; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; /** * This step replaces a data stream backing index with the target index, as part of the data stream's backing indices. @@ -36,11 +39,12 @@ public class ReplaceDataStreamBackingIndexStep extends ClusterStateActionStep { public static final String NAME = "replace-datastream-backing-index"; private static final Logger logger = LogManager.getLogger(ReplaceDataStreamBackingIndexStep.class); - private final String targetIndexPrefix; + private final BiFunction targetIndexNameSupplier; - public ReplaceDataStreamBackingIndexStep(StepKey key, StepKey nextStepKey, String targetIndexPrefix) { + public ReplaceDataStreamBackingIndexStep(StepKey key, StepKey nextStepKey, + BiFunction targetIndexNameSupplier) { super(key, nextStepKey); - this.targetIndexPrefix = targetIndexPrefix; + this.targetIndexNameSupplier = targetIndexNameSupplier; } @Override @@ -48,15 +52,12 @@ public boolean isRetryable() { return true; } - public String getTargetIndexPrefix() { - return targetIndexPrefix; + public BiFunction getTargetIndexNameSupplier() { + return targetIndexNameSupplier; } @Override public ClusterState performAction(Index index, ClusterState clusterState) { - String originalIndex = index.getName(); - final String targetIndexName = targetIndexPrefix + originalIndex; - IndexMetadata originalIndexMetadata = clusterState.metadata().index(index); if (originalIndexMetadata == null) { // Index must have been since deleted, skip the shrink action @@ -64,6 +65,8 @@ public ClusterState performAction(Index index, ClusterState clusterState) { return clusterState; } + String originalIndex = index.getName(); + String targetIndexName = targetIndexNameSupplier.apply(originalIndex, fromIndexMetadata(originalIndexMetadata)); String policyName = originalIndexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME); IndexAbstraction indexAbstraction = clusterState.metadata().getIndicesLookup().get(index.getName()); assert indexAbstraction != null : "invalid cluster metadata. index [" + index.getName() + "] was not found"; @@ -99,7 +102,7 @@ public ClusterState performAction(Index index, ClusterState clusterState) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), targetIndexPrefix); + return Objects.hash(super.hashCode(), targetIndexNameSupplier); } @Override @@ -112,6 +115,6 @@ public boolean equals(Object obj) { } ReplaceDataStreamBackingIndexStep other = (ReplaceDataStreamBackingIndexStep) obj; return super.equals(obj) && - Objects.equals(targetIndexPrefix, other.targetIndexPrefix); + Objects.equals(targetIndexNameSupplier, other.targetIndexNameSupplier); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java index 5b454cafef2c3..8ff3b220641f8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java @@ -220,7 +220,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { WaitForIndexColorStep waitForGreenIndexHealthStep = new WaitForIndexColorStep(waitForGreenRestoredIndexKey, copyMetadataKey, ClusterHealthStatus.GREEN, getRestoredIndexPrefix(waitForGreenRestoredIndexKey)); CopyExecutionStateStep copyMetadataStep = new CopyExecutionStateStep(copyMetadataKey, copyLifecyclePolicySettingKey, - getRestoredIndexPrefix(copyMetadataKey), nextStepKey); + (index, executionState) -> getRestoredIndexPrefix(copyMetadataKey) + index, nextStepKey); CopySettingsStep copySettingsStep = new CopySettingsStep(copyLifecyclePolicySettingKey, dataStreamCheckBranchingKey, getRestoredIndexPrefix(copyLifecyclePolicySettingKey), LifecycleSettings.LIFECYCLE_NAME); BranchingStep isDataStreamBranchingStep = new BranchingStep(dataStreamCheckBranchingKey, swapAliasesKey, replaceDataStreamIndexKey, @@ -230,7 +230,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { return indexAbstraction.getParentDataStream() != null; }); ReplaceDataStreamBackingIndexStep replaceDataStreamBackingIndex = new ReplaceDataStreamBackingIndexStep(replaceDataStreamIndexKey, - deleteIndexKey, getRestoredIndexPrefix(replaceDataStreamIndexKey)); + deleteIndexKey, (index, executionState) -> getRestoredIndexPrefix(replaceDataStreamIndexKey) + index); DeleteStep deleteSourceIndexStep = new DeleteStep(deleteIndexKey, null, client); // sending this step to null as the restored index (which will after this step essentially be the source index) was sent to the next // key after we restored the lifecycle execution state diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java index 59eed5885b8b0..a004f754c4753 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java @@ -17,7 +17,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; @@ -30,6 +29,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; + /** * A {@link LifecycleAction} which shrinks the index. */ @@ -37,7 +38,6 @@ public class ShrinkAction implements LifecycleAction { private static final Logger logger = LogManager.getLogger(ShrinkAction.class); public static final String NAME = "shrink"; - public static final String SHRUNKEN_INDEX_PREFIX = "shrink-"; public static final ParseField NUMBER_OF_SHARDS_FIELD = new ParseField("number_of_shards"); private static final ParseField MAX_PRIMARY_SHARD_SIZE = new ParseField("max_primary_shard_size"); public static final String CONDITIONAL_SKIP_SHRINK_STEP = BranchingStep.NAME + "-check-prerequisites"; @@ -143,12 +143,12 @@ public boolean isSafeAction() { @Override public List toSteps(Client client, String phase, Step.StepKey nextStepKey) { - Settings readOnlySettings = Settings.builder().put(IndexMetadata.SETTING_BLOCKS_WRITE, true).build(); - StepKey preShrinkBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_SHRINK_STEP); StepKey checkNotWriteIndex = new StepKey(phase, NAME, CheckNotDataStreamWriteIndexStep.NAME); StepKey waitForNoFollowerStepKey = new StepKey(phase, NAME, WaitForNoFollowersStep.NAME); StepKey readOnlyKey = new StepKey(phase, NAME, ReadOnlyAction.NAME); + StepKey cleanupShrinkIndexKey = new StepKey(phase, NAME, CleanupShrinkIndexStep.NAME); + StepKey generateShrinkIndexNameKey = new StepKey(phase, NAME, GenerateUniqueIndexNameStep.NAME); StepKey setSingleNodeKey = new StepKey(phase, NAME, SetSingleNodeAllocateStep.NAME); StepKey allocationRoutedKey = new StepKey(phase, NAME, CheckShrinkReadyStep.NAME); StepKey shrinkKey = new StepKey(phase, NAME, ShrinkStep.NAME); @@ -177,14 +177,33 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) CheckNotDataStreamWriteIndexStep checkNotWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNotWriteIndex, waitForNoFollowerStepKey); WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, readOnlyKey, client); - UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, setSingleNodeKey, client, readOnlySettings); + ReadOnlyStep readOnlyStep = new ReadOnlyStep(readOnlyKey, cleanupShrinkIndexKey, client); + // we generate a unique shrink index name but we also retry if the allocation of the shrunk index is not possible, so we want to + // delete the "previously generated" shrink index (this is a no-op if it's the first run of the action and he haven't generated a + // shrink index name) + CleanupShrinkIndexStep cleanupShrinkIndexStep = new CleanupShrinkIndexStep(cleanupShrinkIndexKey, generateShrinkIndexNameKey, + client); + // generate a unique shrink index name and store it in the ILM execution state + GenerateUniqueIndexNameStep generateUniqueIndexNameStep = + new GenerateUniqueIndexNameStep(generateShrinkIndexNameKey, setSingleNodeKey, SHRUNKEN_INDEX_PREFIX, + (generatedIndexName, lifecycleStateBuilder) -> lifecycleStateBuilder.setShrinkIndexName(generatedIndexName)); + // choose a node to collocate the source index in preparation for shrink SetSingleNodeAllocateStep setSingleNodeStep = new SetSingleNodeAllocateStep(setSingleNodeKey, allocationRoutedKey, client); - CheckShrinkReadyStep checkShrinkReadyStep = new CheckShrinkReadyStep(allocationRoutedKey, shrinkKey); - ShrinkStep shrink = new ShrinkStep(shrinkKey, enoughShardsKey, client, numberOfShards, maxPrimaryShardSize, - SHRUNKEN_INDEX_PREFIX); - ShrunkShardsAllocatedStep allocated = new ShrunkShardsAllocatedStep(enoughShardsKey, copyMetadataKey, SHRUNKEN_INDEX_PREFIX); + + // wait for the source shards to be collocated before attempting to shrink the index. we're waiting until a configured threshold is + // breached (controlled by LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD) at which point we rewind to the + // "set-single-node-allocation" step to choose another node to host the shrink operation + ClusterStateWaitUntilThresholdStep checkShrinkReadyStep = new ClusterStateWaitUntilThresholdStep( + new CheckShrinkReadyStep(allocationRoutedKey, shrinkKey), setSingleNodeKey); + ShrinkStep shrink = new ShrinkStep(shrinkKey, enoughShardsKey, client, numberOfShards, maxPrimaryShardSize); + + // wait until the shrunk index is recovered. we again wait until the configured threshold is breached and if the shrunk index has + // not successfully recovered until then, we rewind to the "cleanup-shrink-index" step to delete this unsuccessful shrunk index + // and retry the operation by generating a new shrink index name and attempting to shrink again + ClusterStateWaitUntilThresholdStep allocated = new ClusterStateWaitUntilThresholdStep( + new ShrunkShardsAllocatedStep(enoughShardsKey, copyMetadataKey), cleanupShrinkIndexKey); CopyExecutionStateStep copyMetadata = new CopyExecutionStateStep(copyMetadataKey, dataStreamCheckBranchingKey, - SHRUNKEN_INDEX_PREFIX, isShrunkIndexKey); + ShrinkIndexNameSupplier::getShrinkIndexName, isShrunkIndexKey); // by the time we get to this step we have 2 indices, the source and the shrunken one. we now need to choose an index // swapping strategy such that the shrunken index takes the place of the source index (which is also deleted). // if the source index is part of a data stream it's a matter of replacing it with the shrunken index one in the data stream and @@ -196,14 +215,15 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) assert indexAbstraction != null : "invalid cluster metadata. index [" + index.getName() + "] was not found"; return indexAbstraction.getParentDataStream() != null; }); - ShrinkSetAliasStep aliasSwapAndDelete = new ShrinkSetAliasStep(aliasKey, isShrunkIndexKey, client, SHRUNKEN_INDEX_PREFIX); + ShrinkSetAliasStep aliasSwapAndDelete = new ShrinkSetAliasStep(aliasKey, isShrunkIndexKey, client); ReplaceDataStreamBackingIndexStep replaceDataStreamBackingIndex = new ReplaceDataStreamBackingIndexStep(replaceDataStreamIndexKey, - deleteIndexKey, SHRUNKEN_INDEX_PREFIX); + deleteIndexKey, ShrinkIndexNameSupplier::getShrinkIndexName); DeleteStep deleteSourceIndexStep = new DeleteStep(deleteIndexKey, isShrunkIndexKey, client); - ShrunkenIndexCheckStep waitOnShrinkTakeover = new ShrunkenIndexCheckStep(isShrunkIndexKey, nextStepKey, SHRUNKEN_INDEX_PREFIX); - return Arrays.asList(conditionalSkipShrinkStep, checkNotWriteIndexStep, waitForNoFollowersStep, readOnlyStep, setSingleNodeStep, - checkShrinkReadyStep, shrink, allocated, copyMetadata, isDataStreamBranchingStep, aliasSwapAndDelete, waitOnShrinkTakeover, - replaceDataStreamBackingIndex, deleteSourceIndexStep); + ShrunkenIndexCheckStep waitOnShrinkTakeover = new ShrunkenIndexCheckStep(isShrunkIndexKey, nextStepKey); + return Arrays.asList(conditionalSkipShrinkStep, checkNotWriteIndexStep, waitForNoFollowersStep, readOnlyStep, + cleanupShrinkIndexStep, generateUniqueIndexNameStep, setSingleNodeStep, checkShrinkReadyStep, shrink, allocated, + copyMetadata, isDataStreamBranchingStep, aliasSwapAndDelete, waitOnShrinkTakeover, replaceDataStreamBackingIndex, + deleteSourceIndexStep); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplier.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplier.java new file mode 100644 index 0000000000000..0fba484223b1c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm; + +public final class ShrinkIndexNameSupplier { + + public static final String SHRUNKEN_INDEX_PREFIX = "shrink-"; + + private ShrinkIndexNameSupplier() { + } + + /** + * This could be seen as a getter with a fallback, as it'll attempt to read the shrink index name from the provided lifecycle execution + * state. If no shrink index name was recorded in the execution state it'll construct the shrink index name by prepending + * {@link #SHRUNKEN_INDEX_PREFIX} to the source index name. + */ + static String getShrinkIndexName(String sourceIndexName, LifecycleExecutionState lifecycleState) { + String shrunkenIndexName = lifecycleState.getShrinkIndexName(); + if (shrunkenIndexName == null) { + // this is for BWC reasons for polices that are in the middle of executing the shrink action when the update to generated + // names happens + shrunkenIndexName = SHRUNKEN_INDEX_PREFIX + sourceIndexName; + } + return shrunkenIndexName; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java index 54dbec9098a3e..a0c9923a1369b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java @@ -10,8 +10,8 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; -import java.util.Objects; - +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.getShrinkIndexName; import static org.elasticsearch.xpack.core.ilm.SwapAliasesAndDeleteSourceIndexStep.deleteSourceIndexAndTransferAliases; /** @@ -20,23 +20,23 @@ */ public class ShrinkSetAliasStep extends AsyncRetryDuringSnapshotActionStep { public static final String NAME = "aliases"; - private String shrunkIndexPrefix; - public ShrinkSetAliasStep(StepKey key, StepKey nextStepKey, Client client, String shrunkIndexPrefix) { + public ShrinkSetAliasStep(StepKey key, StepKey nextStepKey, Client client) { super(key, nextStepKey, client); - this.shrunkIndexPrefix = shrunkIndexPrefix; } - String getShrunkIndexPrefix() { - return shrunkIndexPrefix; + @Override + public boolean isRetryable() { + return true; } @Override public void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentState, Listener listener) { // get source index - String index = indexMetadata.getIndex().getName(); + String indexName = indexMetadata.getIndex().getName(); // get target shrink index - String targetIndexName = shrunkIndexPrefix + index; + LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata); + String targetIndexName = getShrinkIndexName(indexName, lifecycleState); deleteSourceIndexAndTransferAliases(getClient(), indexMetadata, getMasterTimeout(currentState), targetIndexName, listener); } @@ -45,21 +45,4 @@ public boolean indexSurvives() { return false; } - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), shrunkIndexPrefix); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ShrinkSetAliasStep other = (ShrinkSetAliasStep) obj; - return super.equals(obj) && - Objects.equals(shrunkIndexPrefix, other.shrunkIndexPrefix); - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkStep.java index a17d55873ac93..0541480828235 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkStep.java @@ -6,6 +6,8 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.client.Client; @@ -17,22 +19,29 @@ import java.util.Objects; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.getShrinkIndexName; + /** * Shrinks an index, using a prefix prepended to the original index name for the name of the shrunken index. */ public class ShrinkStep extends AsyncActionStep { public static final String NAME = "shrink"; + private static final Logger logger = LogManager.getLogger(ShrinkStep.class); + private Integer numberOfShards; private ByteSizeValue maxPrimaryShardSize; - private String shrunkIndexPrefix; public ShrinkStep(StepKey key, StepKey nextStepKey, Client client, Integer numberOfShards, - ByteSizeValue maxPrimaryShardSize, String shrunkIndexPrefix) { + ByteSizeValue maxPrimaryShardSize) { super(key, nextStepKey, client); this.numberOfShards = numberOfShards; this.maxPrimaryShardSize = maxPrimaryShardSize; - this.shrunkIndexPrefix = shrunkIndexPrefix; + } + + @Override + public boolean isRetryable() { + return true; } public Integer getNumberOfShards() { @@ -43,10 +52,6 @@ public ByteSizeValue getMaxPrimaryShardSize() { return maxPrimaryShardSize; } - String getShrunkIndexPrefix() { - return shrunkIndexPrefix; - } - @Override public void performAction(IndexMetadata indexMetadata, ClusterState currentState, ClusterStateObserver observer, Listener listener) { LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetadata); @@ -55,6 +60,15 @@ public void performAction(IndexMetadata indexMetadata, ClusterState currentState "] is missing lifecycle date"); } + String shrunkenIndexName = getShrinkIndexName(indexMetadata.getIndex().getName(), lifecycleState); + if (currentState.metadata().index(shrunkenIndexName) != null) { + logger.warn("skipping [{}] step for index [{}] as part of policy [{}] as the shrunk index [{}] already exists", + ShrinkStep.NAME, indexMetadata.getIndex().getName(), + LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()), shrunkenIndexName); + listener.onResponse(true); + return; + } + String lifecycle = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()); Settings.Builder builder = Settings.builder(); @@ -67,7 +81,6 @@ public void performAction(IndexMetadata indexMetadata, ClusterState currentState } Settings relevantTargetSettings = builder.build(); - String shrunkenIndexName = shrunkIndexPrefix + indexMetadata.getIndex().getName(); ResizeRequest resizeRequest = new ResizeRequest(shrunkenIndexName, indexMetadata.getIndex().getName()) .masterNodeTimeout(getMasterTimeout(currentState)); resizeRequest.setMaxPrimaryShardSize(maxPrimaryShardSize); @@ -84,7 +97,7 @@ public void performAction(IndexMetadata indexMetadata, ClusterState currentState @Override public int hashCode() { - return Objects.hash(super.hashCode(), numberOfShards, maxPrimaryShardSize, shrunkIndexPrefix); + return Objects.hash(super.hashCode(), numberOfShards, maxPrimaryShardSize); } @Override @@ -98,8 +111,7 @@ public boolean equals(Object obj) { ShrinkStep other = (ShrinkStep) obj; return super.equals(obj) && Objects.equals(numberOfShards, other.numberOfShards) && - Objects.equals(maxPrimaryShardSize, other.maxPrimaryShardSize) && - Objects.equals(shrunkIndexPrefix, other.shrunkIndexPrefix); + Objects.equals(maxPrimaryShardSize, other.maxPrimaryShardSize); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStep.java index dc8b53662f932..ef4d45b9b7fb3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStep.java @@ -6,8 +6,11 @@ */ package org.elasticsearch.xpack.core.ilm; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -18,32 +21,45 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.getShrinkIndexName; + /** * Checks whether all shards in a shrunken index have been successfully allocated. */ public class ShrunkShardsAllocatedStep extends ClusterStateWaitStep { public static final String NAME = "shrunk-shards-allocated"; - private String shrunkIndexPrefix; - public ShrunkShardsAllocatedStep(StepKey key, StepKey nextStepKey, String shrunkIndexPrefix) { + private static final Logger logger = LogManager.getLogger(ShrunkShardsAllocatedStep.class); + + public ShrunkShardsAllocatedStep(StepKey key, StepKey nextStepKey) { super(key, nextStepKey); - this.shrunkIndexPrefix = shrunkIndexPrefix; } - String getShrunkIndexPrefix() { - return shrunkIndexPrefix; + @Override + public boolean isRetryable() { + return true; } @Override public Result isConditionMet(Index index, ClusterState clusterState) { + IndexMetadata indexMetadata = clusterState.metadata().index(index); + if (indexMetadata == null) { + // Index must have been since deleted, ignore it + logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), index.getName()); + return new Result(false, null); + } + + LifecycleExecutionState lifecycleState = LifecycleExecutionState.fromIndexMetadata(indexMetadata); + String shrunkenIndexName = getShrinkIndexName(indexMetadata.getIndex().getName(), lifecycleState); + // We only want to make progress if all shards of the shrunk index are // active - boolean indexExists = clusterState.metadata().index(shrunkIndexPrefix + index.getName()) != null; + boolean indexExists = clusterState.metadata().index(shrunkenIndexName) != null; if (indexExists == false) { return new Result(false, new Info(false, -1, false)); } - boolean allShardsActive = ActiveShardCount.ALL.enoughShardsActive(clusterState, shrunkIndexPrefix + index.getName()); - int numShrunkIndexShards = clusterState.metadata().index(shrunkIndexPrefix + index.getName()).getNumberOfShards(); + boolean allShardsActive = ActiveShardCount.ALL.enoughShardsActive(clusterState, shrunkenIndexName); + int numShrunkIndexShards = clusterState.metadata().index(shrunkenIndexName).getNumberOfShards(); if (allShardsActive) { return new Result(true, null); } else { @@ -51,23 +67,6 @@ public Result isConditionMet(Index index, ClusterState clusterState) { } } - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), shrunkIndexPrefix); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ShrunkShardsAllocatedStep other = (ShrunkShardsAllocatedStep) obj; - return super.equals(obj) && Objects.equals(shrunkIndexPrefix, other.shrunkIndexPrefix); - } - public static final class Info implements ToXContentObject { private final int actualShards; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStep.java index c5ad3ecfb7147..86bf3649967c4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStep.java @@ -20,6 +20,9 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.getShrinkIndexName; + /** * Verifies that an index was created through a shrink operation, rather than created some other way. * Also checks the name of the index to ensure it aligns with what is expected from an index shrunken via a previous step. @@ -27,15 +30,14 @@ public class ShrunkenIndexCheckStep extends ClusterStateWaitStep { public static final String NAME = "is-shrunken-index"; private static final Logger logger = LogManager.getLogger(ShrunkenIndexCheckStep.class); - private String shrunkIndexPrefix; - public ShrunkenIndexCheckStep(StepKey key, StepKey nextStepKey, String shrunkIndexPrefix) { + public ShrunkenIndexCheckStep(StepKey key, StepKey nextStepKey) { super(key, nextStepKey); - this.shrunkIndexPrefix = shrunkIndexPrefix; } - String getShrunkIndexPrefix() { - return shrunkIndexPrefix; + @Override + public boolean isRetryable() { + return true; } @Override @@ -51,7 +53,10 @@ public Result isConditionMet(Index index, ClusterState clusterState) { if (Strings.isNullOrEmpty(shrunkenIndexSource)) { throw new IllegalStateException("step[" + NAME + "] is checking an un-shrunken index[" + index.getName() + "]"); } - boolean isConditionMet = index.getName().equals(shrunkIndexPrefix + shrunkenIndexSource) && + + LifecycleExecutionState lifecycleState = fromIndexMetadata(idxMeta); + String targetIndexName = getShrinkIndexName(shrunkenIndexSource, lifecycleState); + boolean isConditionMet = index.getName().equals(targetIndexName) && clusterState.metadata().index(shrunkenIndexSource) == null; if (isConditionMet) { return new Result(true, null); @@ -60,24 +65,6 @@ public Result isConditionMet(Index index, ClusterState clusterState) { } } - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), shrunkIndexPrefix); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ShrunkenIndexCheckStep other = (ShrunkenIndexCheckStep) obj; - return super.equals(obj) && - Objects.equals(shrunkIndexPrefix, other.shrunkIndexPrefix); - } - public static final class Info implements ToXContentObject { private final String originalIndexName; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java new file mode 100644 index 0000000000000..39d34e0b456b9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm.step.info; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * A simple object that allows a `message` field to be transferred to `XContent`. + */ +public class SingleMessageFieldInfo implements ToXContentObject { + + private final String message; + + static final ParseField MESSAGE = new ParseField("message"); + + public SingleMessageFieldInfo(String message) { + this.message = message; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(MESSAGE.getPreferredName(), message); + builder.endObject(); + return builder; + } + + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SingleMessageFieldInfo that = (SingleMessageFieldInfo) o; + return Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStepTests.java new file mode 100644 index 0000000000000..8b7d3712c22cf --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupShrinkIndexStepTests.java @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.xpack.core.ilm.Step.StepKey; + +import java.util.Map; + +import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState; +import static org.elasticsearch.xpack.core.ilm.GenerateUniqueIndexNameStep.generateValidIndexName; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.is; + +public class CleanupShrinkIndexStepTests extends AbstractStepTestCase { + + @Override + public CleanupShrinkIndexStep createRandomInstance() { + StepKey stepKey = randomStepKey(); + StepKey nextStepKey = randomStepKey(); + return new CleanupShrinkIndexStep(stepKey, nextStepKey, client); + } + + @Override + protected CleanupShrinkIndexStep copyInstance(CleanupShrinkIndexStep instance) { + return new CleanupShrinkIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); + } + + @Override + public CleanupShrinkIndexStep mutateInstance(CleanupShrinkIndexStep instance) { + StepKey key = instance.getKey(); + StepKey nextKey = instance.getNextStepKey(); + switch (between(0, 1)) { + case 0: + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new CleanupShrinkIndexStep(key, nextKey, instance.getClient()); + } + + public void testPerformActionDoesntFailIfShrinkingIndexNameIsMissing() { + String indexName = randomAlphaOfLength(10); + String policyName = "test-ilm-policy"; + + IndexMetadata.Builder indexMetadataBuilder = + IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)); + + IndexMetadata indexMetadata = indexMetadataBuilder.build(); + + ClusterState clusterState = + ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetadata, true).build()).build(); + + CleanupShrinkIndexStep cleanupShrinkIndexStep = createRandomInstance(); + cleanupShrinkIndexStep.performAction(indexMetadata, clusterState, null, new AsyncActionStep.Listener() { + @Override + public void onResponse(boolean complete) { + assertThat(complete, is(true)); + } + + @Override + public void onFailure(Exception e) { + fail("expecting the step to not report any failure if there isn't any shrink index name stored in the ILM execution " + + "state but got:" + e.getMessage()); + } + }); + } + + public void testPerformAction() { + String indexName = randomAlphaOfLength(10); + String policyName = "test-ilm-policy"; + String shrinkIndexName = generateValidIndexName("shrink-", indexName); + Map ilmCustom = org.elasticsearch.common.collect.Map.of("shrink_index_name", shrinkIndexName); + + IndexMetadata.Builder indexMetadataBuilder = + IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName)) + .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)); + IndexMetadata indexMetadata = indexMetadataBuilder.build(); + + ClusterState clusterState = + ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetadata, true).build()).build(); + + try (NoOpClient client = getDeleteIndexRequestAssertingClient(shrinkIndexName)) { + CleanupShrinkIndexStep step = new CleanupShrinkIndexStep(randomStepKey(), randomStepKey(), client); + step.performAction(indexMetadata, clusterState, null, new AsyncActionStep.Listener() { + @Override + public void onResponse(boolean complete) { + } + + @Override + public void onFailure(Exception e) { + } + }); + } + } + + public void testDeleteSkippedIfManagedIndexIsShrunkAndSourceDoesntExist() throws Exception { + String sourceIndex = randomAlphaOfLength(10); + String policyName = "test-ilm-policy"; + String shrinkIndexName = generateValidIndexName("shrink-", sourceIndex); + Map ilmCustom = org.elasticsearch.common.collect.Map.of("shrink_index_name", shrinkIndexName); + + IndexMetadata.Builder shrunkIndexMetadataBuilder = IndexMetadata.builder(shrinkIndexName) + .settings( + settings(Version.CURRENT) + .put(LifecycleSettings.LIFECYCLE_NAME, policyName) + .put(IndexMetadata.INDEX_RESIZE_SOURCE_NAME_KEY, sourceIndex) + ) + .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)); + IndexMetadata shrunkIndexMetadata = shrunkIndexMetadataBuilder.build(); + + ClusterState clusterState = + ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(shrunkIndexMetadata, true).build()).build(); + + try (NoOpClient client = getFailingIfCalledClient()) { + CleanupShrinkIndexStep step = new CleanupShrinkIndexStep(randomStepKey(), randomStepKey(), client); + step.performAction(shrunkIndexMetadata, clusterState, null, new AsyncActionStep.Listener() { + @Override + public void onResponse(boolean complete) { + assertThat(complete, is(true)); + } + + @Override + public void onFailure(Exception e) { + + } + }); + } + } + + private NoOpClient getDeleteIndexRequestAssertingClient(String shrinkIndexName) { + return new NoOpClient(getTestName()) { + @Override + protected void doExecute(ActionType action, + Request request, + ActionListener listener) { + assertThat(action.name(), is(DeleteIndexAction.NAME)); + assertTrue(request instanceof DeleteIndexRequest); + assertThat(((DeleteIndexRequest) request).indices(), arrayContaining(shrinkIndexName)); + } + }; + } + + private NoOpClient getFailingIfCalledClient() { + return new NoOpClient(getTestName()) { + @Override + protected void doExecute(ActionType action, + Request request, + ActionListener listener) { + throw new IllegalStateException("not expecting client to be called, but received request [" + request + "] for action [" + + action + "]"); + } + }; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java new file mode 100644 index 0000000000000..1a6d6cc57a512 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.Index; +import org.elasticsearch.xpack.core.ilm.Step.StepKey; +import org.elasticsearch.xpack.core.ilm.step.info.SingleMessageFieldInfo; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Collections; +import java.util.UUID; + +import static org.elasticsearch.xpack.core.ilm.ClusterStateWaitUntilThresholdStep.waitedMoreThanThresholdLevel; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; +import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class ClusterStateWaitUntilThresholdStepTests extends AbstractStepTestCase { + + @Override + public ClusterStateWaitUntilThresholdStep createRandomInstance() { + ClusterStateWaitStep stepToExecute = new WaitForActiveShardsStep(randomStepKey(), randomStepKey()); + return new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); + } + + @Override + public ClusterStateWaitUntilThresholdStep mutateInstance(ClusterStateWaitUntilThresholdStep instance) { + ClusterStateWaitStep stepToExecute = instance.getStepToExecute(); + StepKey nextKeyOnThreshold = instance.getNextKeyOnThreshold(); + + switch (between(0, 1)) { + case 0: + stepToExecute = randomValueOtherThan(stepToExecute, () -> new WaitForActiveShardsStep(randomStepKey(), randomStepKey())); + break; + case 1: + nextKeyOnThreshold = randomValueOtherThan(nextKeyOnThreshold, AbstractStepTestCase::randomStepKey); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + + return new ClusterStateWaitUntilThresholdStep(stepToExecute, nextKeyOnThreshold); + } + + @Override + public ClusterStateWaitUntilThresholdStep copyInstance(ClusterStateWaitUntilThresholdStep instance) { + return new ClusterStateWaitUntilThresholdStep(instance.getStepToExecute(), instance.getNextKeyOnThreshold()); + } + + public void testIndexIsMissingReturnsIncompleteResult() { + WaitForIndexingCompleteStep stepToExecute = new WaitForIndexingCompleteStep(randomStepKey(), randomStepKey()); + ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); + ClusterStateWaitStep.Result result = underTest.isConditionMet(new Index("testName", UUID.randomUUID().toString()), + ClusterState.EMPTY_STATE); + assertThat(result.isComplete(), is(false)); + assertThat(result.getInfomationContext(), nullValue()); + } + + public void testIsConditionMetForUnderlyingStep() { + { + // threshold is not breached and the underlying step condition is met + IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") + .settings( + settings(Version.CURRENT) + .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true") + .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "480h") + ) + .putCustom(ILM_CUSTOM_METADATA_KEY, org.elasticsearch.common.collect.Map + .of("step_time", String.valueOf(System.currentTimeMillis()))) + .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("cluster")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + WaitForIndexingCompleteStep stepToExecute = new WaitForIndexingCompleteStep(randomStepKey(), randomStepKey()); + ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); + + ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + } + + { + // threshold is not breached and the underlying step condition is NOT met + IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") + .settings( + settings(Version.CURRENT) + .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "false") + .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "48h") + ) + .putCustom(ILM_CUSTOM_METADATA_KEY, org.elasticsearch.common.collect.Map + .of("step_time", String.valueOf(System.currentTimeMillis()))) + .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("cluster")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + WaitForIndexingCompleteStep stepToExecute = new WaitForIndexingCompleteStep(randomStepKey(), randomStepKey()); + ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); + ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); + + assertThat(result.isComplete(), is(false)); + assertThat(result.getInfomationContext(), notNullValue()); + WaitForIndexingCompleteStep.IndexingNotCompleteInfo info = + (WaitForIndexingCompleteStep.IndexingNotCompleteInfo) result.getInfomationContext(); + assertThat(info.getMessage(), equalTo("waiting for the [index.lifecycle.indexing_complete] setting to be set to " + + "true on the leader index, it is currently [false]")); + } + + { + // underlying step is executed once even if the threshold is breached and the underlying complete result is returned + IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") + .settings( + settings(Version.CURRENT) + .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true") + .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "1s") + ) + .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(ILM_CUSTOM_METADATA_KEY, org.elasticsearch.common.collect.Map.of("step_time", String.valueOf(1234L))) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("cluster")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + WaitForIndexingCompleteStep stepToExecute = new WaitForIndexingCompleteStep(randomStepKey(), randomStepKey()); + StepKey nextKeyOnThresholdBreach = randomStepKey(); + ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, nextKeyOnThresholdBreach); + + ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), nullValue()); + assertThat(underTest.getNextStepKey(), is(not(nextKeyOnThresholdBreach))); + assertThat(underTest.getNextStepKey(), is(stepToExecute.getNextStepKey())); + } + + { + // underlying step is executed once even if the threshold is breached, but because the underlying step result is false the + // step under test will return `complete` (becuase the threshold is breached and we don't want to wait anymore) + IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") + .settings( + settings(Version.CURRENT) + .put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "false") + .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "1h") + ) + .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(ILM_CUSTOM_METADATA_KEY, org.elasticsearch.common.collect.Map.of("step_time", String.valueOf(1234L))) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("cluster")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + StepKey currentStepKey = randomStepKey(); + WaitForIndexingCompleteStep stepToExecute = new WaitForIndexingCompleteStep(currentStepKey, randomStepKey()); + StepKey nextKeyOnThresholdBreach = randomStepKey(); + ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, nextKeyOnThresholdBreach); + ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); + + assertThat(result.isComplete(), is(true)); + assertThat(result.getInfomationContext(), notNullValue()); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInfomationContext(); + assertThat(info.getMessage(), + equalTo("[" + currentStepKey.getName() + "] lifecycle step, as part of [" + currentStepKey.getAction() + "] " + + "action, for index [follower-index] executed for more than [1h]. Abandoning execution and moving to the next " + + "fallback step [" + nextKeyOnThresholdBreach + "]")); + + // the next step must change to the provided one when the threshold is breached + assertThat(underTest.getNextStepKey(), is(nextKeyOnThresholdBreach)); + } + } + + public void testWaitedMoreThanThresholdLevelMath() { + long epochMillis = 1552684146542L; // Friday, 15 March 2019 21:09:06.542 + Clock clock = Clock.fixed(Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault()); + TimeValue retryThreshold = TimeValue.timeValueHours(1); + + { + // step time is "2 hours ago" with a threshold of 1 hour - the threshold level is breached + LifecycleExecutionState executionState = + new LifecycleExecutionState.Builder().setStepTime(epochMillis - TimeValue.timeValueHours(2).millis()).build(); + boolean thresholdBreached = waitedMoreThanThresholdLevel(retryThreshold, executionState, clock); + assertThat(thresholdBreached, is(true)); + } + + { + // step time is "10 minutes ago" with a threshold of 1 hour - the threshold level is NOT breached + LifecycleExecutionState executionState = + new LifecycleExecutionState.Builder().setStepTime(epochMillis - TimeValue.timeValueMinutes(10).millis()).build(); + boolean thresholdBreached = waitedMoreThanThresholdLevel(retryThreshold, executionState, clock); + assertThat(thresholdBreached, is(false)); + } + + { + // if no threshold is configured we'll never report the threshold is breached + LifecycleExecutionState executionState = + new LifecycleExecutionState.Builder().setStepTime(epochMillis - TimeValue.timeValueHours(2).millis()).build(); + boolean thresholdBreached = waitedMoreThanThresholdLevel(null, executionState, clock); + assertThat(thresholdBreached, is(false)); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java index 3af56dcfe3bbf..36e13677337b4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.util.Map; +import java.util.function.BiFunction; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionStateTests.createCustomMetadata; @@ -27,14 +28,14 @@ protected CopyExecutionStateStep createRandomInstance() { StepKey nextStepKey = randomStepKey(); String shrunkIndexPrefix = randomAlphaOfLength(10); StepKey targetNextStepKey = randomStepKey(); - return new CopyExecutionStateStep(stepKey, nextStepKey, shrunkIndexPrefix, targetNextStepKey); + return new CopyExecutionStateStep(stepKey, nextStepKey, (index, state) -> shrunkIndexPrefix + index, targetNextStepKey); } @Override protected CopyExecutionStateStep mutateInstance(CopyExecutionStateStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - String shrunkIndexPrefix = instance.getTargetIndexPrefix(); + BiFunction indexNameSupplier = instance.getTargetIndexNameSupplier(); StepKey targetNextStepKey = instance.getTargetNextStepKey(); switch (between(0, 2)) { @@ -45,7 +46,7 @@ protected CopyExecutionStateStep mutateInstance(CopyExecutionStateStep instance) nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; case 2: - shrunkIndexPrefix += randomAlphaOfLength(5); + indexNameSupplier = (index, state) -> randomAlphaOfLengthBetween(11, 15) + index; break; case 3: targetNextStepKey = new StepKey(targetNextStepKey.getPhase(), targetNextStepKey.getAction(), @@ -55,12 +56,12 @@ protected CopyExecutionStateStep mutateInstance(CopyExecutionStateStep instance) throw new AssertionError("Illegal randomisation branch"); } - return new CopyExecutionStateStep(key, nextKey, shrunkIndexPrefix, targetNextStepKey); + return new CopyExecutionStateStep(key, nextKey, indexNameSupplier, targetNextStepKey); } @Override protected CopyExecutionStateStep copyInstance(CopyExecutionStateStep instance) { - return new CopyExecutionStateStep(instance.getKey(), instance.getNextStepKey(), instance.getTargetIndexPrefix(), + return new CopyExecutionStateStep(instance.getKey(), instance.getNextStepKey(), instance.getTargetIndexNameSupplier(), instance.getTargetNextStepKey()); } @@ -74,7 +75,8 @@ public void testPerformAction() { .numberOfReplicas(randomIntBetween(1,5)) .putCustom(ILM_CUSTOM_METADATA_KEY, customMetadata) .build(); - IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(step.getTargetIndexPrefix() + indexName) + IndexMetadata shrunkIndexMetadata = + IndexMetadata.builder(step.getTargetIndexNameSupplier().apply(indexName, LifecycleExecutionState.builder().build())) .settings(settings(Version.CURRENT)).numberOfShards(randomIntBetween(1,5)) .numberOfReplicas(randomIntBetween(1,5)) .build(); @@ -88,7 +90,8 @@ public void testPerformAction() { LifecycleExecutionState oldIndexData = LifecycleExecutionState.fromIndexMetadata(originalIndexMetadata); LifecycleExecutionState newIndexData = LifecycleExecutionState - .fromIndexMetadata(newClusterState.metadata().index(step.getTargetIndexPrefix() + indexName)); + .fromIndexMetadata(newClusterState.metadata().index(step.getTargetIndexNameSupplier().apply(indexName, + LifecycleExecutionState.builder().build()))); StepKey targetNextStepKey = step.getTargetNextStepKey(); assertEquals(newIndexData.getLifecycleDate(), oldIndexData.getLifecycleDate()); @@ -98,6 +101,7 @@ public void testPerformAction() { assertEquals(newIndexData.getSnapshotRepository(), oldIndexData.getSnapshotRepository()); assertEquals(newIndexData.getSnapshotName(), oldIndexData.getSnapshotName()); } + public void testPerformActionWithNoTarget() { CopyExecutionStateStep step = createRandomInstance(); String indexName = randomAlphaOfLengthBetween(5, 20); @@ -117,6 +121,8 @@ public void testPerformActionWithNoTarget() { () -> step.performAction(originalIndexMetadata.getIndex(), originalClusterState)); assertThat(e.getMessage(), equalTo("unable to copy execution state from [" + - indexName + "] to [" + step.getTargetIndexPrefix() + indexName + "] as target index does not exist")); + indexName + "] to [" + + step.getTargetIndexNameSupplier().apply(originalIndexMetadata.getIndex().getName(), LifecycleExecutionState.builder().build()) + + "] as target index does not exist")); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStepTests.java new file mode 100644 index 0000000000000..78ee9ab12316a --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateUniqueIndexNameStepTests.java @@ -0,0 +1,215 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasMetadata; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.indices.InvalidIndexNameException; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.Builder; + +import java.util.Locale; +import java.util.function.BiFunction; + +import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState; +import static org.elasticsearch.xpack.core.ilm.GenerateUniqueIndexNameStep.ILLEGAL_INDEXNAME_CHARS_REGEX; +import static org.elasticsearch.xpack.core.ilm.GenerateUniqueIndexNameStep.generateValidIndexName; +import static org.elasticsearch.xpack.core.ilm.GenerateUniqueIndexNameStep.generateValidIndexSuffix; +import static org.elasticsearch.xpack.core.ilm.GenerateUniqueIndexNameStep.validateGeneratedIndexName; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +public class GenerateUniqueIndexNameStepTests extends AbstractStepTestCase { + + @Override + protected GenerateUniqueIndexNameStep createRandomInstance() { + return new GenerateUniqueIndexNameStep(randomStepKey(), randomStepKey(), randomAlphaOfLengthBetween(5, 10), lifecycleStateSetter()); + } + + private static BiFunction lifecycleStateSetter() { + return (generatedIndexName, lifecycleStateBuilder) -> lifecycleStateBuilder.setShrinkIndexName(generatedIndexName); + } + + @Override + protected GenerateUniqueIndexNameStep mutateInstance(GenerateUniqueIndexNameStep instance) { + Step.StepKey key = instance.getKey(); + Step.StepKey nextKey = instance.getNextStepKey(); + String prefix = instance.prefix(); + + switch (between(0, 2)) { + case 0: + key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 2: + prefix = randomValueOtherThan(prefix, () -> randomAlphaOfLengthBetween(5, 10)); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new GenerateUniqueIndexNameStep(key, nextKey, prefix, lifecycleStateSetter()); + } + + @Override + protected GenerateUniqueIndexNameStep copyInstance(GenerateUniqueIndexNameStep instance) { + return new GenerateUniqueIndexNameStep(instance.getKey(), instance.getNextStepKey(), instance.prefix(), + instance.lifecycleStateSetter()); + } + + public void testPerformAction() { + String indexName = randomAlphaOfLength(10); + String policyName = "test-ilm-policy"; + IndexMetadata.Builder indexMetadataBuilder = + IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)); + + final IndexMetadata indexMetadata = indexMetadataBuilder.build(); + ClusterState clusterState = ClusterState.builder(emptyClusterState()) + .metadata(Metadata.builder().put(indexMetadata, false).build()).build(); + + GenerateUniqueIndexNameStep generateUniqueIndexNameStep = createRandomInstance(); + ClusterState newClusterState = generateUniqueIndexNameStep.performAction(indexMetadata.getIndex(), clusterState); + + LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(newClusterState.metadata().index(indexName)); + assertThat("the " + GenerateUniqueIndexNameStep.NAME + " step must generate an index name", executionState.getShrinkIndexName(), + notNullValue()); + assertThat(executionState.getShrinkIndexName(), containsString(indexName)); + assertThat(executionState.getShrinkIndexName(), startsWith(generateUniqueIndexNameStep.prefix())); + } + + public void testGenerateValidIndexName() { + String prefix = randomAlphaOfLengthBetween(5, 15); + String indexName = randomAlphaOfLengthBetween(5, 100); + + String generatedValidIndexName = GenerateUniqueIndexNameStep.generateValidIndexName(prefix, indexName); + assertThat(generatedValidIndexName, startsWith(prefix)); + assertThat(generatedValidIndexName, containsString(indexName)); + try { + MetadataCreateIndexService.validateIndexOrAliasName(generatedValidIndexName, InvalidIndexNameException::new); + } catch (InvalidIndexNameException e) { + fail("generated index name [" + generatedValidIndexName + "] which is invalid due to [" + e.getDetailedMessage() + "]"); + } + } + + public void testGenerateValidIndexSuffix() { + { + String indexSuffix = generateValidIndexSuffix(() -> UUIDs.randomBase64UUID().toLowerCase(Locale.ROOT)); + assertThat(indexSuffix, notNullValue()); + assertThat(indexSuffix.length(), greaterThanOrEqualTo(1)); + assertThat(indexSuffix.matches(ILLEGAL_INDEXNAME_CHARS_REGEX), is(false)); + } + + { + IllegalArgumentException illegalArgumentException = expectThrows(IllegalArgumentException.class, + () -> generateValidIndexSuffix(() -> "****???><><>,# \\/:||")); + assertThat(illegalArgumentException.getMessage(), is("unable to generate random index name suffix")); + } + + { + assertThat(generateValidIndexSuffix(() -> "LegalChars|||# *"), is("legalchars")); + } + } + + public void testValidateGeneratedIndexName() { + { + assertThat(validateGeneratedIndexName( + generateValidIndexName(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 150)), ClusterState.EMPTY_STATE + ), nullValue()); + } + + { + // index name is validated (invalid chars etc) + String generatedIndexName = generateValidIndexName("_prefix-", randomAlphaOfLengthBetween(5, 150)); + assertThat(validateGeneratedIndexName(generatedIndexName, ClusterState.EMPTY_STATE).validationErrors(), containsInAnyOrder( + "Invalid index name [" + generatedIndexName + "], must not start with '_', '-', or '+'")); + } + + { + // index name is validated (invalid chars etc) + String generatedIndexName = generateValidIndexName("shrink-", "shrink-indexName-random###"); + assertThat(validateGeneratedIndexName(generatedIndexName, ClusterState.EMPTY_STATE).validationErrors(), containsInAnyOrder( + "Invalid index name [" + generatedIndexName + "], must not contain '#'")); + } + + { + // generated index already exists as a standalone index + String generatedIndexName = generateValidIndexName(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 150)); + IndexMetadata indexMetadata = IndexMetadata.builder(generatedIndexName) + .settings(settings(Version.CURRENT)).numberOfShards(randomIntBetween(1,5)) + .numberOfReplicas(randomIntBetween(1,5)) + .build(); + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .metadata(Metadata.builder() + .put(indexMetadata, false)) + .build(); + + ActionRequestValidationException validationException = validateGeneratedIndexName(generatedIndexName, clusterState); + assertThat(validationException, notNullValue()); + assertThat(validationException.validationErrors(), containsInAnyOrder("the index name we generated [" + generatedIndexName + + "] already exists")); + } + + { + // generated index name already exists as an index (cluster state routing table is also populated) + String generatedIndexName = generateValidIndexName(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 150)); + IndexMetadata indexMetadata = IndexMetadata.builder(generatedIndexName) + .settings(settings(Version.CURRENT)).numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(1, 5)) + .build(); + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata).build()) + .metadata(Metadata.builder().put(indexMetadata, false)) + .build(); + + ActionRequestValidationException validationException = validateGeneratedIndexName(generatedIndexName, clusterState); + assertThat(validationException, notNullValue()); + assertThat(validationException.validationErrors(), containsInAnyOrder("the index name we generated [" + generatedIndexName + + "] already exists"));; + } + + { + // generated index name already exists as an alias to another index + String generatedIndexName = generateValidIndexName(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(5, 150)); + IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLengthBetween(10, 30)) + .settings(settings(Version.CURRENT)).numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(1, 5)) + .putAlias(AliasMetadata.builder(generatedIndexName).build()) + .build(); + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .metadata(Metadata.builder() + .put(indexMetadata, false)) + .build(); + + ActionRequestValidationException validationException = validateGeneratedIndexName(generatedIndexName, clusterState); + assertThat(validationException, notNullValue()); + assertThat(validationException.validationErrors(), containsInAnyOrder("the index name we generated [" + generatedIndexName + + "] already exists as alias")); + } + } + + public void testParseOriginationDateFromGeneratedIndexName() { + String indexName = "testIndex-2021.03.13-000001"; + String generateValidIndexName = generateValidIndexName("shrink-", indexName); + long extractedDateMillis = IndexLifecycleOriginationDateParser.parseIndexNameAndExtractDate(generateValidIndexName); + assertThat(extractedDateMillis, is(DateFormatter.forPattern("uuuu.MM.dd").parseMillis("2021.03.13"))); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java index 3d00f0db0304e..a1cd2602234c4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java @@ -59,6 +59,7 @@ private static IndexLifecycleExplainResponse randomManagedIndexExplainResponse() stepNull ? null : randomNonNegativeLong(), stepNull ? null : randomAlphaOfLength(10), stepNull ? null : randomAlphaOfLength(10), + stepNull ? null : randomAlphaOfLength(10), randomBoolean() ? null : new BytesArray(new RandomStepInfo(() -> randomAlphaOfLength(10)).toString()), randomBoolean() ? null : PhaseExecutionInfoTests.randomPhaseExecutionInfo("")); } @@ -80,6 +81,7 @@ public void testInvalidStepDetails() { randomBoolean() ? null : randomNonNegativeLong(), randomBoolean() ? null : randomAlphaOfLength(10), randomBoolean() ? null : randomAlphaOfLength(10), + randomBoolean() ? null : randomAlphaOfLength(10), randomBoolean() ? null : new BytesArray(new RandomStepInfo(() -> randomAlphaOfLength(10)).toString()), randomBoolean() ? null : PhaseExecutionInfoTests.randomPhaseExecutionInfo(""))); assertThat(exception.getMessage(), startsWith("managed index response must have complete step details")); @@ -122,11 +124,12 @@ protected IndexLifecycleExplainResponse mutateInstance(IndexLifecycleExplainResp Long stepTime = instance.getStepTime(); String repositoryName = instance.getRepositoryName(); String snapshotName = instance.getSnapshotName(); + String shrinkIndexName = instance.getShrinkIndexName(); boolean managed = instance.managedByILM(); BytesReference stepInfo = instance.getStepInfo(); PhaseExecutionInfo phaseExecutionInfo = instance.getPhaseExecutionInfo(); if (managed) { - switch (between(0, 13)) { + switch (between(0, 14)) { case 0: index = index + randomAlphaOfLengthBetween(1, 5); break; @@ -184,12 +187,15 @@ protected IndexLifecycleExplainResponse mutateInstance(IndexLifecycleExplainResp case 13: snapshotName = randomValueOtherThan(snapshotName, () -> randomAlphaOfLengthBetween(5, 10)); break; + case 14: + shrinkIndexName = randomValueOtherThan(shrinkIndexName, () -> randomAlphaOfLengthBetween(5, 10)); + break; default: throw new AssertionError("Illegal randomisation branch"); } return IndexLifecycleExplainResponse.newManagedIndexResponse(index, policy, policyTime, phase, action, step, failedStep, - isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo, - phaseExecutionInfo); + isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, + shrinkIndexName, stepInfo, phaseExecutionInfo); } else { switch (between(0, 1)) { case 0: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStepTests.java index 0ce800792d5e2..1093ab47030a1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ReplaceDataStreamBackingIndexStepTests.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.UUID; +import java.util.function.BiFunction; import static org.elasticsearch.cluster.DataStreamTestHelper.createTimestampField; import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState; @@ -25,14 +26,15 @@ public class ReplaceDataStreamBackingIndexStepTests extends AbstractStepTestCase @Override protected ReplaceDataStreamBackingIndexStep createRandomInstance() { - return new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), randomAlphaOfLengthBetween(1, 10)); + String prefix = randomAlphaOfLengthBetween(1, 10); + return new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), (index, state) -> prefix + index); } @Override protected ReplaceDataStreamBackingIndexStep mutateInstance(ReplaceDataStreamBackingIndexStep instance) { Step.StepKey key = instance.getKey(); Step.StepKey nextKey = instance.getNextStepKey(); - String indexPrefix = instance.getTargetIndexPrefix(); + BiFunction indexNameSupplier = instance.getTargetIndexNameSupplier(); switch (between(0, 2)) { case 0: @@ -42,17 +44,17 @@ protected ReplaceDataStreamBackingIndexStep mutateInstance(ReplaceDataStreamBack nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; case 2: - indexPrefix = randomValueOtherThan(indexPrefix, () -> randomAlphaOfLengthBetween(1, 10)); + indexNameSupplier = (index, state) -> randomAlphaOfLengthBetween(11, 15) + index; break; default: throw new AssertionError("Illegal randomisation branch"); } - return new ReplaceDataStreamBackingIndexStep(key, nextKey, indexPrefix); + return new ReplaceDataStreamBackingIndexStep(key, nextKey, indexNameSupplier); } @Override protected ReplaceDataStreamBackingIndexStep copyInstance(ReplaceDataStreamBackingIndexStep instance) { - return new ReplaceDataStreamBackingIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getTargetIndexPrefix()); + return new ReplaceDataStreamBackingIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getTargetIndexNameSupplier()); } public void testPerformActionThrowsExceptionIfIndexIsNotPartOfDataStream() { @@ -158,7 +160,7 @@ public void testPerformAction() { ).build(); ReplaceDataStreamBackingIndexStep replaceSourceIndexStep = - new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), indexPrefix); + new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), (index, state) -> indexPrefix + index); ClusterState newState = replaceSourceIndexStep.performAction(sourceIndexMetadata.getIndex(), clusterState); DataStream updatedDataStream = newState.metadata().dataStreams().get(dataStreamName); assertThat(updatedDataStream.getIndices().size(), is(2)); @@ -203,7 +205,7 @@ public void testPerformActionSameOriginalTargetError() { ).build(); ReplaceDataStreamBackingIndexStep replaceSourceIndexStep = - new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), indexPrefix); + new ReplaceDataStreamBackingIndexStep(randomStepKey(), randomStepKey(), (index, state) -> indexPrefix + index); IllegalStateException ex = expectThrows( IllegalStateException.class, () -> replaceSourceIndexStep.performAction(sourceIndexMetadata.getIndex(), clusterState) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java index 1ea3438c4e964..a776e2fd462aa 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java @@ -22,6 +22,8 @@ import java.util.List; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; public class ShrinkActionTests extends AbstractActionTestCase { @@ -146,21 +148,23 @@ public void testToSteps() { StepKey nextStepKey = new StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10)); List steps = action.toSteps(null, phase, nextStepKey); - assertThat(steps.size(), equalTo(14)); + assertThat(steps.size(), equalTo(16)); StepKey expectedFirstKey = new StepKey(phase, ShrinkAction.NAME, ShrinkAction.CONDITIONAL_SKIP_SHRINK_STEP); StepKey expectedSecondKey = new StepKey(phase, ShrinkAction.NAME, CheckNotDataStreamWriteIndexStep.NAME); StepKey expectedThirdKey = new StepKey(phase, ShrinkAction.NAME, WaitForNoFollowersStep.NAME); StepKey expectedFourthKey = new StepKey(phase, ShrinkAction.NAME, ReadOnlyAction.NAME); - StepKey expectedFifthKey = new StepKey(phase, ShrinkAction.NAME, SetSingleNodeAllocateStep.NAME); - StepKey expectedSixthKey = new StepKey(phase, ShrinkAction.NAME, CheckShrinkReadyStep.NAME); - StepKey expectedSeventhKey = new StepKey(phase, ShrinkAction.NAME, ShrinkStep.NAME); - StepKey expectedEighthKey = new StepKey(phase, ShrinkAction.NAME, ShrunkShardsAllocatedStep.NAME); - StepKey expectedNinthKey = new StepKey(phase, ShrinkAction.NAME, CopyExecutionStateStep.NAME); - StepKey expectedTenthKey = new StepKey(phase, ShrinkAction.NAME, ShrinkAction.CONDITIONAL_DATASTREAM_CHECK_KEY); - StepKey expectedEleventhKey = new StepKey(phase, ShrinkAction.NAME, ShrinkSetAliasStep.NAME); - StepKey expectedTwelveKey = new StepKey(phase, ShrinkAction.NAME, ShrunkenIndexCheckStep.NAME); - StepKey expectedThirteenKey = new StepKey(phase, ShrinkAction.NAME, ReplaceDataStreamBackingIndexStep.NAME); - StepKey expectedFourteenKey = new StepKey(phase, ShrinkAction.NAME, DeleteStep.NAME); + StepKey expectedFifthKey = new StepKey(phase, ShrinkAction.NAME, CleanupShrinkIndexStep.NAME); + StepKey expectedSixthKey = new StepKey(phase, ShrinkAction.NAME, GenerateUniqueIndexNameStep.NAME); + StepKey expectedSeventhKey = new StepKey(phase, ShrinkAction.NAME, SetSingleNodeAllocateStep.NAME); + StepKey expectedEighthKey = new StepKey(phase, ShrinkAction.NAME, CheckShrinkReadyStep.NAME); + StepKey expectedNinthKey = new StepKey(phase, ShrinkAction.NAME, ShrinkStep.NAME); + StepKey expectedTenthKey = new StepKey(phase, ShrinkAction.NAME, ShrunkShardsAllocatedStep.NAME); + StepKey expectedEleventhKey = new StepKey(phase, ShrinkAction.NAME, CopyExecutionStateStep.NAME); + StepKey expectedTwelveKey = new StepKey(phase, ShrinkAction.NAME, ShrinkAction.CONDITIONAL_DATASTREAM_CHECK_KEY); + StepKey expectedThirteenKey = new StepKey(phase, ShrinkAction.NAME, ShrinkSetAliasStep.NAME); + StepKey expectedFourteenKey = new StepKey(phase, ShrinkAction.NAME, ShrunkenIndexCheckStep.NAME); + StepKey expectedFifteenKey = new StepKey(phase, ShrinkAction.NAME, ReplaceDataStreamBackingIndexStep.NAME); + StepKey expectedSixteenKey = new StepKey(phase, ShrinkAction.NAME, DeleteStep.NAME); assertTrue(steps.get(0) instanceof BranchingStep); assertThat(steps.get(0).getKey(), equalTo(expectedFirstKey)); @@ -176,58 +180,65 @@ public void testToSteps() { assertThat(steps.get(2).getKey(), equalTo(expectedThirdKey)); assertThat(steps.get(2).getNextStepKey(), equalTo(expectedFourthKey)); - assertTrue(steps.get(3) instanceof UpdateSettingsStep); + assertTrue(steps.get(3) instanceof ReadOnlyStep); assertThat(steps.get(3).getKey(), equalTo(expectedFourthKey)); assertThat(steps.get(3).getNextStepKey(), equalTo(expectedFifthKey)); - assertTrue(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.get(((UpdateSettingsStep)steps.get(3)).getSettings())); - assertTrue(steps.get(4) instanceof SetSingleNodeAllocateStep); + assertTrue(steps.get(4) instanceof CleanupShrinkIndexStep); assertThat(steps.get(4).getKey(), equalTo(expectedFifthKey)); assertThat(steps.get(4).getNextStepKey(), equalTo(expectedSixthKey)); - assertTrue(steps.get(5) instanceof CheckShrinkReadyStep); + assertTrue(steps.get(5) instanceof GenerateUniqueIndexNameStep); assertThat(steps.get(5).getKey(), equalTo(expectedSixthKey)); assertThat(steps.get(5).getNextStepKey(), equalTo(expectedSeventhKey)); - assertTrue(steps.get(6) instanceof ShrinkStep); + assertTrue(steps.get(6) instanceof SetSingleNodeAllocateStep); assertThat(steps.get(6).getKey(), equalTo(expectedSeventhKey)); assertThat(steps.get(6).getNextStepKey(), equalTo(expectedEighthKey)); - assertThat(((ShrinkStep) steps.get(6)).getShrunkIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); - assertTrue(steps.get(7) instanceof ShrunkShardsAllocatedStep); + assertTrue(steps.get(7) instanceof ClusterStateWaitUntilThresholdStep); + assertThat(((ClusterStateWaitUntilThresholdStep) steps.get(7)).getStepToExecute(), is(instanceOf(CheckShrinkReadyStep.class))); + // assert in case the threshold is breached we go back to the "cleanup shrunk index" step + assertThat(((ClusterStateWaitUntilThresholdStep) steps.get(7)).getNextKeyOnThreshold(), is(expectedSeventhKey)); assertThat(steps.get(7).getKey(), equalTo(expectedEighthKey)); assertThat(steps.get(7).getNextStepKey(), equalTo(expectedNinthKey)); - assertThat(((ShrunkShardsAllocatedStep) steps.get(7)).getShrunkIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); - assertTrue(steps.get(8) instanceof CopyExecutionStateStep); + assertTrue(steps.get(8) instanceof ShrinkStep); assertThat(steps.get(8).getKey(), equalTo(expectedNinthKey)); assertThat(steps.get(8).getNextStepKey(), equalTo(expectedTenthKey)); - assertThat(((CopyExecutionStateStep) steps.get(8)).getTargetIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); - assertTrue(steps.get(9) instanceof BranchingStep); + assertTrue(steps.get(9) instanceof ClusterStateWaitUntilThresholdStep); assertThat(steps.get(9).getKey(), equalTo(expectedTenthKey)); - expectThrows(IllegalStateException.class, () -> steps.get(9).getNextStepKey()); - assertThat(((BranchingStep) steps.get(9)).getNextStepKeyOnFalse(), equalTo(expectedEleventhKey)); - assertThat(((BranchingStep) steps.get(9)).getNextStepKeyOnTrue(), equalTo(expectedThirteenKey)); + assertThat(steps.get(9).getNextStepKey(), equalTo(expectedEleventhKey)); + assertThat(((ClusterStateWaitUntilThresholdStep) steps.get(9)).getStepToExecute(), is(instanceOf(ShrunkShardsAllocatedStep.class))); + // assert in case the threshold is breached we go back to the "cleanup shrunk index" step + assertThat(((ClusterStateWaitUntilThresholdStep) steps.get(9)).getNextKeyOnThreshold(), is(expectedFifthKey)); - assertTrue(steps.get(10) instanceof ShrinkSetAliasStep); + assertTrue(steps.get(10) instanceof CopyExecutionStateStep); assertThat(steps.get(10).getKey(), equalTo(expectedEleventhKey)); assertThat(steps.get(10).getNextStepKey(), equalTo(expectedTwelveKey)); - assertThat(((ShrinkSetAliasStep) steps.get(10)).getShrunkIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); - assertTrue(steps.get(11) instanceof ShrunkenIndexCheckStep); + assertTrue(steps.get(11) instanceof BranchingStep); assertThat(steps.get(11).getKey(), equalTo(expectedTwelveKey)); - assertThat(steps.get(11).getNextStepKey(), equalTo(nextStepKey)); - assertThat(((ShrunkenIndexCheckStep) steps.get(11)).getShrunkIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); + expectThrows(IllegalStateException.class, () -> steps.get(11).getNextStepKey()); + assertThat(((BranchingStep) steps.get(11)).getNextStepKeyOnFalse(), equalTo(expectedThirteenKey)); + assertThat(((BranchingStep) steps.get(11)).getNextStepKeyOnTrue(), equalTo(expectedFifteenKey)); - assertTrue(steps.get(12) instanceof ReplaceDataStreamBackingIndexStep); + assertTrue(steps.get(12) instanceof ShrinkSetAliasStep); assertThat(steps.get(12).getKey(), equalTo(expectedThirteenKey)); assertThat(steps.get(12).getNextStepKey(), equalTo(expectedFourteenKey)); - assertThat(((ReplaceDataStreamBackingIndexStep) steps.get(12)).getTargetIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX)); - assertTrue(steps.get(13) instanceof DeleteStep); + assertTrue(steps.get(13) instanceof ShrunkenIndexCheckStep); assertThat(steps.get(13).getKey(), equalTo(expectedFourteenKey)); - assertThat(steps.get(13).getNextStepKey(), equalTo(expectedTwelveKey)); + assertThat(steps.get(13).getNextStepKey(), equalTo(nextStepKey)); + + assertTrue(steps.get(14) instanceof ReplaceDataStreamBackingIndexStep); + assertThat(steps.get(14).getKey(), equalTo(expectedFifteenKey)); + assertThat(steps.get(14).getNextStepKey(), equalTo(expectedSixteenKey)); + + assertTrue(steps.get(15) instanceof DeleteStep); + assertThat(steps.get(15).getKey(), equalTo(expectedSixteenKey)); + assertThat(steps.get(15).getNextStepKey(), equalTo(expectedFourteenKey)); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplierTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplierTests.java new file mode 100644 index 0000000000000..02660ce136b77 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkIndexNameSupplierTests.java @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.getShrinkIndexName; +import static org.hamcrest.Matchers.is; + +public class ShrinkIndexNameSupplierTests extends ESTestCase { + + public void testGetShrinkIndexName() { + String sourceIndexName = "test-index"; + { + // if the lifecycle execution state contains a `shrink_index_name`, that one will be returned + String shrinkIndexName = "the-shrink-index"; + LifecycleExecutionState lifecycleExecutionState = + LifecycleExecutionState.builder().setShrinkIndexName(shrinkIndexName).build(); + + assertThat(getShrinkIndexName(sourceIndexName, lifecycleExecutionState), is(shrinkIndexName)); + } + + { + // if the lifecycle execution state does NOT contain a `shrink_index_name`, `shrink-` will be prefixed to the index name + assertThat(getShrinkIndexName(sourceIndexName, LifecycleExecutionState.builder().build()), + is(SHRUNKEN_INDEX_PREFIX + sourceIndexName)); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java index 0f24594bc4c4b..7f9231d974eca 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java @@ -23,6 +23,7 @@ import java.util.List; import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.equalTo; public class ShrinkSetAliasStepTests extends AbstractStepTestCase { @@ -32,33 +33,29 @@ public ShrinkSetAliasStep createRandomInstance() { StepKey stepKey = randomStepKey(); StepKey nextStepKey = randomStepKey(); String shrunkIndexPrefix = randomAlphaOfLength(10); - return new ShrinkSetAliasStep(stepKey, nextStepKey, client, shrunkIndexPrefix); + return new ShrinkSetAliasStep(stepKey, nextStepKey, client); } @Override public ShrinkSetAliasStep mutateInstance(ShrinkSetAliasStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - String shrunkIndexPrefix = instance.getShrunkIndexPrefix(); - switch (between(0, 2)) { + switch (between(0, 1)) { case 0: key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; case 1: nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; - case 2: - shrunkIndexPrefix += randomAlphaOfLength(5); - break; default: throw new AssertionError("Illegal randomisation branch"); } - return new ShrinkSetAliasStep(key, nextKey, instance.getClient(), shrunkIndexPrefix); + return new ShrinkSetAliasStep(key, nextKey, instance.getClient()); } @Override public ShrinkSetAliasStep copyInstance(ShrinkSetAliasStep instance) { - return new ShrinkSetAliasStep(instance.getKey(), instance.getNextStepKey(), instance.getClient(), instance.getShrunkIndexPrefix()); + return new ShrinkSetAliasStep(instance.getKey(), instance.getNextStepKey(), instance.getClient()); } public void testPerformAction() { @@ -82,7 +79,7 @@ public void testPerformAction() { ShrinkSetAliasStep step = createRandomInstance(); String sourceIndex = indexMetadata.getIndex().getName(); - String shrunkenIndex = step.getShrunkIndexPrefix() + sourceIndex; + String shrunkenIndex = SHRUNKEN_INDEX_PREFIX + sourceIndex; List expectedAliasActions = Arrays.asList( IndicesAliasesRequest.AliasActions.removeIndex().index(sourceIndex), IndicesAliasesRequest.AliasActions.add().index(shrunkenIndex).alias(sourceIndex), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java index 094618eaaf99d..a99d6955f0498 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java @@ -40,7 +40,7 @@ public ShrinkStep createRandomInstance() { maxPrimaryShardSize = new ByteSizeValue(between(1,100)); } String shrunkIndexPrefix = randomAlphaOfLength(10); - return new ShrinkStep(stepKey, nextStepKey, client, numberOfShards, maxPrimaryShardSize, shrunkIndexPrefix); + return new ShrinkStep(stepKey, nextStepKey, client, numberOfShards, maxPrimaryShardSize); } @Override @@ -49,9 +49,8 @@ public ShrinkStep mutateInstance(ShrinkStep instance) { StepKey nextKey = instance.getNextStepKey(); Integer numberOfShards = instance.getNumberOfShards(); ByteSizeValue maxPrimaryShardSize = instance.getMaxPrimaryShardSize(); - String shrunkIndexPrefix = instance.getShrunkIndexPrefix(); - switch (between(0, 3)) { + switch (between(0, 2)) { case 0: key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; @@ -66,20 +65,17 @@ public ShrinkStep mutateInstance(ShrinkStep instance) { maxPrimaryShardSize = new ByteSizeValue(maxPrimaryShardSize.getBytes() + 1); } break; - case 3: - shrunkIndexPrefix += randomAlphaOfLength(5); - break; default: throw new AssertionError("Illegal randomisation branch"); } - return new ShrinkStep(key, nextKey, instance.getClient(), numberOfShards, maxPrimaryShardSize, shrunkIndexPrefix); + return new ShrinkStep(key, nextKey, instance.getClient(), numberOfShards, maxPrimaryShardSize); } @Override public ShrinkStep copyInstance(ShrinkStep instance) { return new ShrinkStep(instance.getKey(), instance.getNextStepKey(), instance.getClient(), instance.getNumberOfShards(), - instance.getMaxPrimaryShardSize(), instance.getShrunkIndexPrefix()); + instance.getMaxPrimaryShardSize()); } public void testPerformAction() throws Exception { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java index fc1c48a87f652..ec0ed77dd816e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java @@ -24,42 +24,39 @@ import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep.Result; import org.elasticsearch.xpack.core.ilm.Step.StepKey; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; + public class ShrunkShardsAllocatedStepTests extends AbstractStepTestCase { @Override public ShrunkShardsAllocatedStep createRandomInstance() { StepKey stepKey = randomStepKey(); StepKey nextStepKey = randomStepKey(); - String shrunkIndexPrefix = randomAlphaOfLength(10); - return new ShrunkShardsAllocatedStep(stepKey, nextStepKey, shrunkIndexPrefix); + return new ShrunkShardsAllocatedStep(stepKey, nextStepKey); } @Override public ShrunkShardsAllocatedStep mutateInstance(ShrunkShardsAllocatedStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - String shrunkIndexPrefix = instance.getShrunkIndexPrefix(); - - switch (between(0, 2)) { - case 0: - key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); - break; - case 1: - nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); - break; - case 2: - shrunkIndexPrefix += randomAlphaOfLength(5); - break; - default: + + switch (between(0, 1)) { + case 0: + key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + case 1: + nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); + break; + default: throw new AssertionError("Illegal randomisation branch"); } - return new ShrunkShardsAllocatedStep(key, nextKey, shrunkIndexPrefix); + return new ShrunkShardsAllocatedStep(key, nextKey); } @Override public ShrunkShardsAllocatedStep copyInstance(ShrunkShardsAllocatedStep instance) { - return new ShrunkShardsAllocatedStep(instance.getKey(), instance.getNextStepKey(), instance.getShrunkIndexPrefix()); + return new ShrunkShardsAllocatedStep(instance.getKey(), instance.getNextStepKey()); } public void testConditionMet() { @@ -71,7 +68,7 @@ public void testConditionMet() { .settings(settings(Version.CURRENT)) .numberOfShards(originalNumberOfShards) .numberOfReplicas(0).build(); - IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(step.getShrunkIndexPrefix() + originalIndexName) + IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(SHRUNKEN_INDEX_PREFIX + originalIndexName) .settings(settings(Version.CURRENT)) .numberOfShards(shrinkNumberOfShards) .numberOfReplicas(0).build(); @@ -112,7 +109,7 @@ public void testConditionNotMetBecauseOfActive() { .settings(settings(Version.CURRENT)) .numberOfShards(originalNumberOfShards) .numberOfReplicas(0).build(); - IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(step.getShrunkIndexPrefix() + originalIndexName) + IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(SHRUNKEN_INDEX_PREFIX + originalIndexName) .settings(settings(Version.CURRENT)) .numberOfShards(shrinkNumberOfShards) .numberOfReplicas(0).build(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java index a2108658e9850..16ae32526204e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep.Result; import org.elasticsearch.xpack.core.ilm.Step.StepKey; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.equalTo; public class ShrunkenIndexCheckStepTests extends AbstractStepTestCase { @@ -23,39 +24,35 @@ public ShrunkenIndexCheckStep createRandomInstance() { StepKey stepKey = randomStepKey(); StepKey nextStepKey = randomStepKey(); String shrunkIndexPrefix = randomAlphaOfLength(10); - return new ShrunkenIndexCheckStep(stepKey, nextStepKey, shrunkIndexPrefix); + return new ShrunkenIndexCheckStep(stepKey, nextStepKey); } @Override public ShrunkenIndexCheckStep mutateInstance(ShrunkenIndexCheckStep instance) { StepKey key = instance.getKey(); StepKey nextKey = instance.getNextStepKey(); - String shrunkIndexPrefix = instance.getShrunkIndexPrefix(); - switch (between(0, 2)) { + switch (between(0, 1)) { case 0: key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; case 1: nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5)); break; - case 2: - shrunkIndexPrefix += randomAlphaOfLength(5); - break; default: throw new AssertionError("Illegal randomisation branch"); } - return new ShrunkenIndexCheckStep(key, nextKey, shrunkIndexPrefix); + return new ShrunkenIndexCheckStep(key, nextKey); } @Override public ShrunkenIndexCheckStep copyInstance(ShrunkenIndexCheckStep instance) { - return new ShrunkenIndexCheckStep(instance.getKey(), instance.getNextStepKey(), instance.getShrunkIndexPrefix()); + return new ShrunkenIndexCheckStep(instance.getKey(), instance.getNextStepKey()); } public void testConditionMet() { ShrunkenIndexCheckStep step = createRandomInstance(); String sourceIndex = randomAlphaOfLengthBetween(1, 10); - IndexMetadata indexMetadata = IndexMetadata.builder(step.getShrunkIndexPrefix() + sourceIndex) + IndexMetadata indexMetadata = IndexMetadata.builder(SHRUNKEN_INDEX_PREFIX + sourceIndex) .settings(settings(Version.CURRENT).put(IndexMetadata.INDEX_RESIZE_SOURCE_NAME_KEY, sourceIndex)) .numberOfShards(1) .numberOfReplicas(0).build(); @@ -94,7 +91,7 @@ public void testConditionNotMetBecauseSourceIndexExists() { .settings(settings(Version.CURRENT)) .numberOfShards(100) .numberOfReplicas(0).build(); - IndexMetadata shrinkIndexMetadata = IndexMetadata.builder(step.getShrunkIndexPrefix() + sourceIndex) + IndexMetadata shrinkIndexMetadata = IndexMetadata.builder(SHRUNKEN_INDEX_PREFIX + sourceIndex) .settings(settings(Version.CURRENT).put(IndexMetadata.INDEX_RESIZE_SOURCE_NAME_KEY, sourceIndex)) .numberOfShards(1) .numberOfReplicas(0).build(); diff --git a/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java b/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java index 37b18eb2bad5e..33bec52d88c7d 100644 --- a/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java +++ b/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java @@ -40,6 +40,7 @@ import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -286,9 +287,7 @@ public void testCcrAndIlmWithRollover() throws Exception { public void testAliasReplicatedOnShrink() throws Exception { final String indexName = "shrink-alias-test"; - final String shrunkenIndexName = "shrink-" + indexName; final String policyName = "shrink-test-policy"; - final int numberOfAliases = randomIntBetween(0, 4); if ("leader".equals(targetCluster)) { @@ -335,6 +334,9 @@ public void testAliasReplicatedOnShrink() throws Exception { // Wait for the setting to get replicated assertBusy(() -> assertThat(getIndexSetting(client(), indexName, "index.lifecycle.indexing_complete"), equalTo("true"))); + assertBusy(() -> assertThat(getShrinkIndexName(client(), indexName) , notNullValue()), 30, TimeUnit.SECONDS); + String shrunkenIndexName = getShrinkIndexName(client(), indexName); + // Wait for the index to continue with its lifecycle and be shrunk assertBusy(() -> assertTrue(indexExists(shrunkenIndexName))); @@ -353,7 +355,6 @@ public void testAliasReplicatedOnShrink() throws Exception { public void testUnfollowInjectedBeforeShrink() throws Exception { final String indexName = "shrink-test"; - final String shrunkenIndexName = "shrink-" + indexName; final String policyName = "shrink-test-policy"; if ("leader".equals(targetCluster)) { @@ -390,6 +391,9 @@ public void testUnfollowInjectedBeforeShrink() throws Exception { // moves through the unfollow and shrink actions so fast that the // index often disappears between assertBusy checks + assertBusy(() -> assertThat(getShrinkIndexName(client(), indexName) , notNullValue()), 1, TimeUnit.MINUTES); + String shrunkenIndexName = getShrinkIndexName(client(), indexName); + // Wait for the index to continue with its lifecycle and be shrunk assertBusy(() -> assertTrue(indexExists(shrunkenIndexName))); @@ -400,8 +404,6 @@ public void testUnfollowInjectedBeforeShrink() throws Exception { public void testCannotShrinkLeaderIndex() throws Exception { String indexName = "shrink-leader-test"; - String shrunkenIndexName = "shrink-" + indexName; - String policyName = "shrink-leader-test-policy"; if ("leader".equals(targetCluster)) { // Set up the policy and index, but don't attach the policy yet, @@ -458,6 +460,8 @@ public void testCannotShrinkLeaderIndex() throws Exception { .build() ); + assertBusy(() -> assertThat(getShrinkIndexName(leaderClient, indexName) , notNullValue()), 30, TimeUnit.SECONDS); + String shrunkenIndexName = getShrinkIndexName(leaderClient, indexName); assertBusy(() -> { // The shrunken index should now be created on the leader... Response shrunkenIndexExistsResponse = leaderClient.performRequest(new Request("HEAD", "/" + shrunkenIndexName)); @@ -471,7 +475,6 @@ public void testCannotShrinkLeaderIndex() throws Exception { } else { fail("unexpected target cluster [" + targetCluster + "]"); } - } public void testILMUnfollowFailsToRemoveRetentionLeases() throws Exception { @@ -802,4 +805,48 @@ private String getSnapshotState(String snapshot) throws IOException { assertThat(snapResponse.get("snapshot"), equalTo(snapshot)); return (String) snapResponse.get("state"); } + + @SuppressWarnings("unchecked") + private static String getShrinkIndexName(RestClient client, String originalIndex) throws InterruptedException, IOException { + String[] shrunkenIndexName = new String[1]; + waitUntil(() -> { + try { + Request explainRequest = new Request("GET", SHRUNKEN_INDEX_PREFIX + "*" + originalIndex + "," + originalIndex + + "/_ilm/explain"); + explainRequest.addParameter("only_errors", Boolean.toString(false)); + explainRequest.addParameter("only_managed", Boolean.toString(false)); + Response response = client.performRequest(explainRequest); + Map responseMap; + try (InputStream is = response.getEntity().getContent()) { + responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + + Map> indexResponse = ((Map>) responseMap.get("indices")); + Map explainIndexResponse = indexResponse.get(originalIndex); + if(explainIndexResponse == null) { + // maybe we swapped the alias from the original index to the shrunken one already + for (Map.Entry> indexToExplainMap : indexResponse.entrySet()) { + // we don't know the exact name of the shrunken index, but we know it starts with the configured prefix + String indexName = indexToExplainMap.getKey(); + if (indexName.startsWith(SHRUNKEN_INDEX_PREFIX) && indexName.contains(originalIndex)) { + explainIndexResponse = indexToExplainMap.getValue(); + break; + } + } + } + + LOGGER.info("--> index {}, explain {}", originalIndex, explainIndexResponse); + if (explainIndexResponse == null) { + return false; + } + shrunkenIndexName[0] = (String) explainIndexResponse.get("shrink_index_name"); + return shrunkenIndexName[0] != null; + } catch (IOException e) { + return false; + } + }, 30, TimeUnit.SECONDS); + assert shrunkenIndexName[0] != null : "lifecycle execution state must contain the target shrink index name for index [" + + originalIndex + "]"; + return shrunkenIndexName[0]; + } } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java index 5d9a3eefdb31a..3152d02e07011 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java @@ -42,13 +42,16 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; import static org.elasticsearch.test.ESTestCase.randomBoolean; +import static org.elasticsearch.test.ESTestCase.waitUntil; import static org.elasticsearch.test.rest.ESRestTestCase.assertOK; import static org.elasticsearch.test.rest.ESRestTestCase.ensureHealth; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -326,4 +329,49 @@ public static String getSnapshotState(RestClient client, String snapshot) throws assertThat(snapResponse.get("snapshot"), equalTo(snapshot)); return (String) snapResponse.get("state"); } + + @SuppressWarnings("unchecked") + @Nullable + public static String waitAndGetShrinkIndexName(RestClient client, String originalIndex) throws InterruptedException { + String[] shrunkenIndexName = new String[1]; + waitUntil(() -> { + try { + // we're including here the case where the original index was already deleted and we have to look for the shrunken index + Request explainRequest = new Request("GET", SHRUNKEN_INDEX_PREFIX + "*" + originalIndex + "," + originalIndex + + "/_ilm/explain"); + explainRequest.addParameter("only_errors", Boolean.toString(false)); + explainRequest.addParameter("only_managed", Boolean.toString(false)); + Response response = client.performRequest(explainRequest); + Map responseMap; + try (InputStream is = response.getEntity().getContent()) { + responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + + Map> indexResponse = ((Map>) responseMap.get("indices")); + Map explainIndexResponse = indexResponse.get(originalIndex); + if(explainIndexResponse == null) { + // maybe we swapped the alias from the original index to the shrunken one already + for (Map.Entry> indexToExplainMap : indexResponse.entrySet()) { + // we don't know the exact name of the shrunken index, but we know it starts with the configured prefix + String indexName = indexToExplainMap.getKey(); + if (indexName.startsWith(SHRUNKEN_INDEX_PREFIX) && indexName.contains(originalIndex)) { + explainIndexResponse = indexToExplainMap.getValue(); + break; + } + } + } + + logger.info("--> index {}, explain {}", originalIndex, indexResponse); + if (explainIndexResponse == null) { + return false; + } + shrunkenIndexName[0] = (String) explainIndexResponse.get("shrink_index_name"); + return shrunkenIndexName[0] != null; + } catch (IOException e) { + return false; + } + }, 30, TimeUnit.SECONDS); + logger.info("--> original index name is [{}], shrunken index name is [{}]", originalIndex, shrunkenIndexName[0]); + return shrunkenIndexName[0]; + } } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java index a0ed894df6941..9ddb3a98a0e3b 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java @@ -47,6 +47,8 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.getStepKeyForIndex; import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument; import static org.elasticsearch.xpack.TimeSeriesRestDriver.rolloverMaxOneDocCondition; +import static org.elasticsearch.xpack.TimeSeriesRestDriver.waitAndGetShrinkIndexName; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -107,7 +109,6 @@ public void testShrinkActionInPolicyWithoutHotPhase() throws Exception { indexDocument(client(), dataStream, true); String backingIndexName = DataStream.getDefaultBackingIndexName(dataStream, 1); - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + backingIndexName; assertBusy(() -> assertThat( "original index must wait in the " + CheckNotDataStreamWriteIndexStep.NAME + " until it is not the write index anymore", explainIndex(client(), backingIndexName).get("step"), is(CheckNotDataStreamWriteIndexStep.NAME)), 30, TimeUnit.SECONDS); @@ -115,6 +116,7 @@ public void testShrinkActionInPolicyWithoutHotPhase() throws Exception { // Manual rollover the original index such that it's not the write index in the data stream anymore rolloverMaxOneDocCondition(client(), dataStream); + String shrunkenIndex = waitAndGetShrinkIndexName(client(), backingIndexName); assertBusy(() -> assertTrue(indexExists(shrunkenIndex)), 30, TimeUnit.SECONDS); assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); assertBusy(() -> assertThat("the original index must've been deleted", indexExists(backingIndexName), is(false))); @@ -127,7 +129,7 @@ public void testShrinkAfterRollover() throws Exception { String backingIndexName = DataStream.getDefaultBackingIndexName(dataStream, 1); String rolloverIndex = DataStream.getDefaultBackingIndexName(dataStream, 2); - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + backingIndexName; + String shrunkenIndex = SHRUNKEN_INDEX_PREFIX + backingIndexName; assertBusy(() -> assertTrue("the rollover action created the rollover index", indexExists(rolloverIndex))); assertBusy(() -> assertFalse("the original index was deleted by the shrink action", indexExists(backingIndexName)), 60, TimeUnit.SECONDS); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index bc9b4a15963ae..ff6d296e41e11 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -71,6 +71,7 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.index; import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument; import static org.elasticsearch.xpack.TimeSeriesRestDriver.updatePolicy; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -96,7 +97,7 @@ public void refreshIndex() { public void testFullPolicy() throws Exception { String originalIndex = index + "-000001"; - String shrunkenOriginalIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + originalIndex; + String shrunkenOriginalIndex = SHRUNKEN_INDEX_PREFIX + originalIndex; String secondIndex = index + "-000002"; createIndexWithSettings(client(), originalIndex, alias, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 2) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeseriesMoveToStepIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeseriesMoveToStepIT.java index 21c6a8877e0bf..77e77e2c41b38 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeseriesMoveToStepIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeseriesMoveToStepIT.java @@ -33,6 +33,8 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.getStepKeyForIndex; import static org.elasticsearch.xpack.TimeSeriesRestDriver.index; import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument; +import static org.elasticsearch.xpack.TimeSeriesRestDriver.waitAndGetShrinkIndexName; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.containsStringIgnoringCase; import static org.hamcrest.Matchers.equalTo; @@ -81,7 +83,7 @@ public void testMoveToAllocateStep() throws Exception { public void testMoveToRolloverStep() throws Exception { String originalIndex = index + "-000001"; - String shrunkenOriginalIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + originalIndex; + String shrunkenOriginalIndex = SHRUNKEN_INDEX_PREFIX + originalIndex; String secondIndex = index + "-000002"; createFullPolicy(client(), policy, TimeValue.timeValueHours(10)); @@ -125,7 +127,6 @@ public void testMoveToRolloverStep() throws Exception { } public void testMoveToInjectedStep() throws Exception { - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(1, null), TimeValue.timeValueHours(12)); createIndexWithSettings(client(), index, alias, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 3) @@ -153,6 +154,7 @@ public void testMoveToInjectedStep() throws Exception { assertOK(client().performRequest(moveToStepRequest)); // Make sure we actually move on to and execute the shrink action + String shrunkenIndex = waitAndGetShrinkIndexName(client(), index); assertBusy(() -> { assertTrue(indexExists(shrunkenIndex)); assertTrue(aliasExists(shrunkenIndex, index)); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java index 8ef32a4bf6e9b..ea50afe78b6ad 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; import org.elasticsearch.xpack.core.ilm.LifecycleAction; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.MigrateAction; import org.elasticsearch.xpack.core.ilm.Phase; import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; @@ -37,6 +38,8 @@ import java.util.concurrent.TimeUnit; import static java.util.Collections.singletonMap; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createIndexWithSettings; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createNewSingletonPolicy; @@ -47,11 +50,13 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.index; import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument; import static org.elasticsearch.xpack.TimeSeriesRestDriver.updatePolicy; +import static org.elasticsearch.xpack.TimeSeriesRestDriver.waitAndGetShrinkIndexName; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; public class ShrinkActionIT extends ESRestTestCase { private static final String FAILED_STEP_RETRY_COUNT_FIELD = "failed_step_retry_count"; + private static final String SHRINK_INDEX_NAME = "shrink_index_name"; private String policy; private String index; @@ -69,17 +74,19 @@ public void testShrinkAction() throws Exception { int numShards = 4; int divisor = randomFrom(2, 4); int expectedFinalShards = numShards / divisor; - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; - createIndexWithSettings(client(), index, alias, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards) + createIndexWithSettings(client(), index, alias, Settings.builder().put(SETTING_NUMBER_OF_SHARDS, numShards) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)); createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(expectedFinalShards, null)); updatePolicy(client(), index, policy); - assertBusy(() -> assertTrue(indexExists(shrunkenIndex)), 30, TimeUnit.SECONDS); - assertBusy(() -> assertTrue(aliasExists(shrunkenIndex, index))); - assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); + + String shrunkenIndexName = waitAndGetShrinkIndexName(client(), index); + assertBusy(() -> assertTrue(indexExists(shrunkenIndexName)), 30, TimeUnit.SECONDS); + assertBusy(() -> assertTrue(aliasExists(shrunkenIndexName, index))); + assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndexName), + equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); assertBusy(() -> { - Map settings = getOnlyIndexSettings(client(), shrunkenIndex); - assertThat(settings.get(IndexMetadata.SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); + Map settings = getOnlyIndexSettings(client(), shrunkenIndexName); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); assertThat(settings.get(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey()), equalTo("true")); assertThat(settings.get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"), nullValue()); }); @@ -88,25 +95,23 @@ public void testShrinkAction() throws Exception { public void testShrinkSameShards() throws Exception { int numberOfShards = randomFrom(1, 2); - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; - createIndexWithSettings(client(), index, alias, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numberOfShards) + createIndexWithSettings(client(), index, alias, Settings.builder().put(SETTING_NUMBER_OF_SHARDS, numberOfShards) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)); createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(numberOfShards, null)); updatePolicy(client(), index, policy); assertBusy(() -> { assertTrue(indexExists(index)); - assertFalse(indexExists(shrunkenIndex)); - assertFalse(aliasExists(shrunkenIndex, index)); Map settings = getOnlyIndexSettings(client(), index); assertThat(getStepKeyForIndex(client(), index), equalTo(PhaseCompleteStep.finalStep("warm").getKey())); - assertThat(settings.get(IndexMetadata.SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(numberOfShards))); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(numberOfShards))); assertNull(settings.get(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey())); assertThat(settings.get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"), nullValue()); + // the shrink action was effectively skipped so there must not be any `shrink_index_name` in the ILM state + assertThat(explainIndex(client(), index).get("shrink_index_name"), nullValue()); }); } public void testShrinkDuringSnapshot() throws Exception { - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; // Create the repository before taking the snapshot. Request request = new Request("PUT", "/_snapshot/repo"); request.setJsonEntity(Strings @@ -124,7 +129,7 @@ public void testShrinkDuringSnapshot() throws Exception { createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(1, null), TimeValue.timeValueMillis(0)); // create index without policy createIndexWithSettings(client(), index, alias, Settings.builder() - .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 2) + .put(SETTING_NUMBER_OF_SHARDS, 2) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) // required so the shrink doesn't wait on SetSingleNodeAllocateStep .put(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_name", "javaRestTest-0")); @@ -137,13 +142,14 @@ public void testShrinkDuringSnapshot() throws Exception { assertOK(client().performRequest(request)); // add policy and expect it to trigger shrink immediately (while snapshot in progress) updatePolicy(client(), index, policy); + String shrunkenIndex = waitAndGetShrinkIndexName(client(), index); // assert that index was shrunk and original index was deleted assertBusy(() -> { assertTrue(indexExists(shrunkenIndex)); assertTrue(aliasExists(shrunkenIndex, index)); Map settings = getOnlyIndexSettings(client(), shrunkenIndex); assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("warm").getKey())); - assertThat(settings.get(IndexMetadata.SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(1))); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(1))); assertThat(settings.get(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey()), equalTo("true")); assertThat(settings.get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"), nullValue()); }, 2, TimeUnit.MINUTES); @@ -157,7 +163,6 @@ public void testShrinkActionInTheHotPhase() throws Exception { int numShards = 2; int expectedFinalShards = 1; String originalIndex = index + "-000001"; - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + originalIndex; // add a policy Map hotActions = org.elasticsearch.common.collect.Map.of( @@ -187,20 +192,20 @@ RolloverAction.NAME, new RolloverAction(null, null, null, 1L), createIndexWithSettings(client(), originalIndex, alias, Settings.builder(), true); index(client(), originalIndex, "_id", "foo", "bar"); + String shrunkenIndex = waitAndGetShrinkIndexName(client(), originalIndex); assertBusy(() -> assertTrue(indexExists(shrunkenIndex)), 30, TimeUnit.SECONDS); assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("hot").getKey()))); assertBusy(() -> { Map settings = getOnlyIndexSettings(client(), shrunkenIndex); - assertThat(settings.get(IndexMetadata.SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); }); } public void testSetSingleNodeAllocationRetriesUntilItSucceeds() throws Exception { int numShards = 2; int expectedFinalShards = 1; - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; createIndexWithSettings(client(), index, alias, Settings.builder() - .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards) + .put(SETTING_NUMBER_OF_SHARDS, numShards) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) .putNull(DataTierAllocationDecider.INDEX_ROUTING_PREFER)); @@ -259,40 +264,74 @@ public void testSetSingleNodeAllocationRetriesUntilItSucceeds() throws Exception "}"); client().performRequest(resetAllocationForIndex); + String shrunkenIndex = waitAndGetShrinkIndexName(client(), index); assertBusy(() -> assertTrue(indexExists(shrunkenIndex)), 30, TimeUnit.SECONDS); assertBusy(() -> assertTrue(aliasExists(shrunkenIndex, index))); assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); } - public void testRetryFailedShrinkAction() throws Exception { + public void testAutomaticRetryFailedShrinkAction() throws Exception { int numShards = 4; int divisor = randomFrom(2, 4); int expectedFinalShards = numShards / divisor; - String shrunkenIndex = ShrinkAction.SHRUNKEN_INDEX_PREFIX + index; - createIndexWithSettings(client(), index, alias, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, numShards) + createIndexWithSettings(client(), index, alias, Settings.builder().put(SETTING_NUMBER_OF_SHARDS, numShards) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)); createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(numShards + randomIntBetween(1, numShards), null)); updatePolicy(client(), index, policy); assertBusy(() -> { String failedStep = getFailedStepForIndex(index); assertThat(failedStep, equalTo(ShrinkStep.NAME)); - }, 30, TimeUnit.SECONDS); + }, 60, TimeUnit.SECONDS); // update policy to be correct createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(expectedFinalShards, null)); updatePolicy(client(), index, policy); - // retry step - Request retryRequest = new Request("POST", index + "/_ilm/retry"); - assertOK(client().performRequest(retryRequest)); - // assert corrected policy is picked up and index is shrunken + String shrunkenIndex = waitAndGetShrinkIndexName(client(), index); assertBusy(() -> assertTrue(indexExists(shrunkenIndex)), 30, TimeUnit.SECONDS); assertBusy(() -> assertTrue(aliasExists(shrunkenIndex, index))); assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrunkenIndex), equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); assertBusy(() -> { Map settings = getOnlyIndexSettings(client(), shrunkenIndex); - assertThat(settings.get(IndexMetadata.SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); + assertThat(settings.get(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey()), equalTo("true")); + assertThat(settings.get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"), nullValue()); + }); + expectThrows(ResponseException.class, () -> indexDocument(client(), index)); + } + + public void testShrinkStepMovesForwardIfShrunkIndexIsCreatedBetweenRetries() throws Exception { + int numShards = 4; + int expectedFinalShards = 1; + createIndexWithSettings(client(), index, alias, Settings.builder().put(SETTING_NUMBER_OF_SHARDS, numShards) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)); + createNewSingletonPolicy(client(), policy, "warm", new ShrinkAction(numShards + randomIntBetween(1, numShards), null)); + updatePolicy(client(), index, policy); + assertBusy(() -> { + String failedStep = getFailedStepForIndex(index); + assertThat(failedStep, equalTo(ShrinkStep.NAME)); + }, 60, TimeUnit.SECONDS); + + String shrinkIndexName = waitAndGetShrinkIndexName(client(), index); + Request shrinkIndexRequest = new Request("POST", index + "/_shrink/" + shrinkIndexName); + shrinkIndexRequest.setEntity(new StringEntity( + "{\"settings\": {\n" + + " \"" + SETTING_NUMBER_OF_SHARDS + "\": 1,\n" + + " \"" + SETTING_NUMBER_OF_REPLICAS + "\": 0,\n" + + " \"" + LifecycleSettings.LIFECYCLE_NAME + "\": \"" + policy + "\",\n" + + " \"" + IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id" + "\": null\n" + + " \n}\n" + + "\n}", ContentType.APPLICATION_JSON)); + client().performRequest(shrinkIndexRequest); + + // assert manually shrunk index is picked up and policy completes successfully + assertBusy(() -> assertTrue(indexExists(shrinkIndexName)), 30, TimeUnit.SECONDS); + assertBusy(() -> assertTrue(aliasExists(shrinkIndexName, index))); + assertBusy(() -> assertThat(getStepKeyForIndex(client(), shrinkIndexName), equalTo(PhaseCompleteStep.finalStep("warm").getKey()))); + assertBusy(() -> { + Map settings = getOnlyIndexSettings(client(), shrinkIndexName); + assertThat(settings.get(SETTING_NUMBER_OF_SHARDS), equalTo(String.valueOf(expectedFinalShards))); assertThat(settings.get(IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.getKey()), equalTo("true")); assertThat(settings.get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"), nullValue()); }); diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java new file mode 100644 index 0000000000000..13d84f967ea74 --- /dev/null +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ilm; + +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ilm.ExplainLifecycleRequest; +import org.elasticsearch.xpack.core.ilm.ExplainLifecycleResponse; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleExplainResponse; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; +import org.elasticsearch.xpack.core.ilm.MigrateAction; +import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; +import org.elasticsearch.xpack.core.ilm.ShrinkAction; +import org.elasticsearch.xpack.core.ilm.ShrunkShardsAllocatedStep; +import org.elasticsearch.xpack.core.ilm.Step; +import org.elasticsearch.xpack.core.ilm.action.ExplainLifecycleAction; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.junit.Before; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; + +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) +public class ClusterStateWaitThresholdBreachTests extends ESIntegTestCase { + + private String policy; + private String managedIndex; + + @Before + public void refreshDataStreamAndPolicy() { + policy = "policy-" + randomAlphaOfLength(5); + managedIndex = "index-" + randomAlphaOfLengthBetween(10, 15).toLowerCase(Locale.ROOT); + } + + @Override + protected boolean ignoreExternalCluster() { + return true; + } + + @Override + protected Collection> nodePlugins() { + return Arrays.asList(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + Settings.Builder settings = Settings.builder().put(super.nodeSettings(nodeOrdinal)); + settings.put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false); + settings.put(XPackSettings.SECURITY_ENABLED.getKey(), false); + settings.put(XPackSettings.WATCHER_ENABLED.getKey(), false); + settings.put(XPackSettings.GRAPH_ENABLED.getKey(), false); + settings.put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s"); + settings.put(LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING.getKey(), false); + settings.put(LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED, false); + return settings.build(); + } + + @Override + protected Settings transportClientSettings() { + Settings.Builder settings = Settings.builder().put(super.transportClientSettings()); + settings.put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false); + settings.put(XPackSettings.SECURITY_ENABLED.getKey(), false); + settings.put(XPackSettings.WATCHER_ENABLED.getKey(), false); + settings.put(XPackSettings.GRAPH_ENABLED.getKey(), false); + return settings.build(); + } + + @Override + protected Collection> transportClientPlugins() { + return nodePlugins(); + } + + public void testWaitInShrunkShardsAllocatedExceedsThreshold() throws Exception { + List masterOnlyNodes = internalCluster().startMasterOnlyNodes(1, Settings.EMPTY); + internalCluster().startDataOnlyNode(); + + int numShards = 2; + { + Phase warmPhase = new Phase("warm", TimeValue.ZERO, org.elasticsearch.common.collect.Map + .of(MigrateAction.NAME, new MigrateAction(false), ShrinkAction.NAME, + new ShrinkAction(numShards + randomIntBetween(1, numShards), null)) + ); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, org.elasticsearch.common.collect.Map.of("warm", warmPhase)); + PutLifecycleAction.Request putLifecycleRequest = new PutLifecycleAction.Request(lifecyclePolicy); + assertAcked(client().execute(PutLifecycleAction.INSTANCE, putLifecycleRequest).get()); + } + + Settings settings = Settings.builder().put(indexSettings()).put(SETTING_NUMBER_OF_SHARDS, numShards) + .put(SETTING_NUMBER_OF_REPLICAS, 0).put(LifecycleSettings.LIFECYCLE_NAME, policy) + // configuring the threshold to the minimum value + .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "1h") + .build(); + CreateIndexResponse res = client().admin().indices().prepareCreate(managedIndex).setSettings(settings).get(); + assertTrue(res.isAcknowledged()); + + String[] firstAttemptShrinkIndexName = new String[1]; + // ILM will retry the shrink step because the number of shards to shrink to is gt the current number of shards + assertBusy(() -> { + ExplainLifecycleRequest explainRequest = new ExplainLifecycleRequest().indices(managedIndex); + ExplainLifecycleResponse explainResponse = client().execute(ExplainLifecycleAction.INSTANCE, + explainRequest).get(); + + IndexLifecycleExplainResponse indexLifecycleExplainResponse = explainResponse.getIndexResponses().get(managedIndex); + assertThat(indexLifecycleExplainResponse.getFailedStepRetryCount(), greaterThanOrEqualTo(1)); + + firstAttemptShrinkIndexName[0] = indexLifecycleExplainResponse.getShrinkIndexName(); + assertThat(firstAttemptShrinkIndexName[0], is(notNullValue())); + }, 30, TimeUnit.SECONDS); + + + // we're manually shrinking the index but configuring a very high number of replicas and waiting for all active shards + // this will make ths shrunk index unable to allocate successfully, so ILM will wait in the `shrunk-shards-allocated` step + ResizeRequest resizeRequest = new ResizeRequest(firstAttemptShrinkIndexName[0], managedIndex); + Settings.Builder builder = Settings.builder(); + // a very high number of replicas, coupled with an `all` wait for active shards configuration will block the shrink action in the + // `shrunk-shards-allocated` step. + builder.put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 42) + .put("index.write.wait_for_active_shards", "all") + .put(LifecycleSettings.LIFECYCLE_NAME, policy) + .put(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id", (String) null) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1); + Settings relevantTargetSettings = builder.build(); + resizeRequest.getTargetIndexRequest().settings(relevantTargetSettings); + client().admin().indices().resizeIndex(resizeRequest).get(); + ensureYellow(firstAttemptShrinkIndexName[0]); + + // let's check ILM for the managed index is waiting in the `shrunk-shards-allocated` step + assertBusy(() -> { + ExplainLifecycleRequest explainRequest = new ExplainLifecycleRequest().indices(managedIndex); + ExplainLifecycleResponse explainResponse = client().execute(ExplainLifecycleAction.INSTANCE, + explainRequest).get(); + + IndexLifecycleExplainResponse indexLifecycleExplainResponse = explainResponse.getIndexResponses().get(managedIndex); + assertThat(indexLifecycleExplainResponse.getStep(), is(ShrunkShardsAllocatedStep.NAME)); + }, 30, TimeUnit.SECONDS); + + // now to the tricky bit + // we'll use the cluster service to issue a move-to-step task in order to manipulate the ILM execution state `step_time` value to + // a very low value (in order to trip the LIFECYCLE_STEP_WAIT_TIME_THRESHOLD threshold and retry the shrink cycle) + IndexMetadata managedIndexMetadata = clusterService().state().metadata().index(managedIndex); + Step.StepKey currentStepKey = new Step.StepKey("warm", ShrinkAction.NAME, ShrunkShardsAllocatedStep.NAME); + + String masterNode = masterOnlyNodes.get(0); + IndexLifecycleService indexLifecycleService = internalCluster().getInstance(IndexLifecycleService.class, masterNode); + ClusterService clusterService = internalCluster().getInstance(ClusterService.class, masterNode); + // moving the step from the current step to the same step - this is only so we update the step time in the ILM execution state to + // an old timestamp so the `1h` wait threshold we configured using LIFECYCLE_STEP_WAIT_TIME_THRESHOLD is breached and a new + // shrink cycle is started + LongSupplier nowWayBackInThePastSupplier = () -> 1234L; + clusterService.submitStateUpdateTask("testing-move-to-step-to-manipulate-step-time", + new MoveToNextStepUpdateTask(managedIndexMetadata.getIndex(), policy, currentStepKey, currentStepKey, + nowWayBackInThePastSupplier, indexLifecycleService.getPolicyRegistry(), state -> { + })); + + String[] secondCycleShrinkIndexName = new String[1]; + assertBusy(() -> { + ExplainLifecycleRequest explainRequest = new ExplainLifecycleRequest().indices(managedIndex); + ExplainLifecycleResponse explainResponse = client().execute(ExplainLifecycleAction.INSTANCE, + explainRequest).get(); + + IndexLifecycleExplainResponse indexLifecycleExplainResponse = explainResponse.getIndexResponses().get(managedIndex); + secondCycleShrinkIndexName[0] = indexLifecycleExplainResponse.getShrinkIndexName(); + assertThat(secondCycleShrinkIndexName[0], notNullValue()); + // ILM generated another shrink index name + assertThat(secondCycleShrinkIndexName[0], not(equalTo(firstAttemptShrinkIndexName[0]))); + }, 30, TimeUnit.SECONDS); + + // the shrink index generated in the first attempt must've been deleted! + assertBusy(() -> assertFalse(indexExists(firstAttemptShrinkIndexName[0]))); + + // at this point, the manged index is looping into the `shrink` step as the action is trying to shrink to a higher number of + // shards than the source index has. we'll update the policy to shrink to 1 shard and this should unblock the policy and it + // should successfully shrink the managed index to the second cycle shrink index name + { + Phase warmPhase = new Phase("warm", TimeValue.ZERO, org.elasticsearch.common.collect.Map.of(MigrateAction.NAME, + new MigrateAction(false), ShrinkAction.NAME, new ShrinkAction(1, null)) + ); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, org.elasticsearch.common.collect.Map.of("warm", warmPhase)); + PutLifecycleAction.Request putLifecycleRequest = new PutLifecycleAction.Request(lifecyclePolicy); + assertAcked(client().execute(PutLifecycleAction.INSTANCE, putLifecycleRequest).get()); + } + + assertBusy(() -> assertTrue(indexExists(secondCycleShrinkIndexName[0])), 30, TimeUnit.SECONDS); + assertBusy(() -> { + ExplainLifecycleRequest explainRequest = new ExplainLifecycleRequest().indices(secondCycleShrinkIndexName[0]); + ExplainLifecycleResponse explainResponse = client().execute(ExplainLifecycleAction.INSTANCE, + explainRequest).get(); + IndexLifecycleExplainResponse indexLifecycleExplainResponse = explainResponse.getIndexResponses() + .get(secondCycleShrinkIndexName[0]); + assertThat(indexLifecycleExplainResponse.getStep(), equalTo(PhaseCompleteStep.NAME)); + }, 30, TimeUnit.SECONDS); + } +} diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java index ba651323c3d4b..2cc0fbfa02752 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java @@ -7,20 +7,19 @@ package org.elasticsearch.xpack.ilm; -import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.template.put.PutComposableIndexTemplateAction; import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.action.DeleteDataStreamAction; import org.elasticsearch.xpack.core.ilm.ExplainLifecycleRequest; import org.elasticsearch.xpack.core.ilm.ExplainLifecycleResponse; import org.elasticsearch.xpack.core.ilm.IndexLifecycleExplainResponse; @@ -31,6 +30,8 @@ import org.elasticsearch.xpack.core.ilm.ShrinkAction; import org.elasticsearch.xpack.core.ilm.action.ExplainLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.datastreams.DataStreamsPlugin; +import org.junit.After; import java.util.Arrays; import java.util.Collection; @@ -39,50 +40,56 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.equalTo; @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, numClientNodes = 0) public class ILMMultiNodeIT extends ESIntegTestCase { + private static final String index = "myindex"; @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class); + return Arrays.asList(LocalStateCompositeXPackPlugin.class, DataStreamsPlugin.class, IndexLifecycle.class); } @Override - protected Collection> transportClientPlugins() { - return nodePlugins(); + protected Settings nodeSettings(int nodeOrdinal) { + return Settings.builder().put(super.nodeSettings(nodeOrdinal)) + .put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false) + .put(XPackSettings.SECURITY_ENABLED.getKey(), false) + .put(XPackSettings.WATCHER_ENABLED.getKey(), false) + .put(XPackSettings.GRAPH_ENABLED.getKey(), false) + .put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s") + // This just generates less churn and makes it easier to read the log file if needed + .put(LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING.getKey(), false) + .put(LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED, false) + .build(); } @Override - protected Settings nodeSettings(int nodeOrdinal) { - Settings.Builder settings = Settings.builder().put(super.nodeSettings(nodeOrdinal)); + protected Settings transportClientSettings() { + Settings.Builder settings = Settings.builder().put(super.transportClientSettings()); settings.put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false); settings.put(XPackSettings.SECURITY_ENABLED.getKey(), false); settings.put(XPackSettings.WATCHER_ENABLED.getKey(), false); settings.put(XPackSettings.GRAPH_ENABLED.getKey(), false); - settings.put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s"); - - // This is necessary to prevent ILM and SLM installing a lifecycle policy, these tests assume a blank slate - settings.put(LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED, false); - settings.put(LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING.getKey(), false); return settings.build(); } @Override - protected boolean ignoreExternalCluster() { - return true; + protected Collection> transportClientPlugins() { + return nodePlugins(); } - @Override - protected Settings transportClientSettings() { - Settings.Builder settings = Settings.builder().put(super.transportClientSettings()); - settings.put(XPackSettings.MACHINE_LEARNING_ENABLED.getKey(), false); - settings.put(XPackSettings.SECURITY_ENABLED.getKey(), false); - settings.put(XPackSettings.WATCHER_ENABLED.getKey(), false); - settings.put(XPackSettings.GRAPH_ENABLED.getKey(), false); - return settings.build(); + @After + public void cleanup() { + try { + client().execute(DeleteDataStreamAction.INSTANCE, new DeleteDataStreamAction.Request(new String[]{index})).get(); + } catch (Exception e) { + // Okay to ignore this + logger.info("failed to clean up data stream", e); + } } public void testShrinkOnTiers() throws Exception { @@ -104,38 +111,44 @@ public void testShrinkOnTiers() throws Exception { .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 2) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) .put(LifecycleSettings.LIFECYCLE_NAME, "shrink-policy") - .put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, "shrink-alias") - .put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, "data_hot") .build(), null, null); ComposableIndexTemplate template = new ComposableIndexTemplate( - Collections.singletonList(index + "*"), + Collections.singletonList(index), t, null, null, null, + null, + new ComposableIndexTemplate.DataStreamTemplate(), null ); client().execute( PutComposableIndexTemplateAction.INSTANCE, new PutComposableIndexTemplateAction.Request("template").indexTemplate(template) ).actionGet(); - client().admin().indices().prepareCreate(index + "-000001") - .addAlias(new Alias("shrink-alias").writeIndex(true)).get(); - client().prepareIndex(index + "-000001", MapperService.SINGLE_MAPPING_NAME) - .setCreate(true).setId("1").setSource("@timestamp", "2020-09-09").get(); + client().prepareIndex(index, "_doc").setCreate(true).setId("1").setSource("@timestamp", "2020-09-09").get(); assertBusy(() -> { - String name = "shrink-" + index + "-000001"; ExplainLifecycleResponse explain = client().execute(ExplainLifecycleAction.INSTANCE, new ExplainLifecycleRequest().indices("*")).get(); logger.info("--> explain: {}", Strings.toString(explain)); - IndexLifecycleExplainResponse indexResp = explain.getIndexResponses().get(name); - assertNotNull(indexResp); - assertThat(indexResp.getPhase(), equalTo("warm")); - assertThat(indexResp.getStep(), equalTo("complete")); - }, 60, TimeUnit.SECONDS); + String backingIndexName = DataStream.getDefaultBackingIndexName(index, 1); + IndexLifecycleExplainResponse indexResp = null; + for (Map.Entry indexNameAndResp : explain.getIndexResponses().entrySet()) { + if (indexNameAndResp.getKey().startsWith(SHRUNKEN_INDEX_PREFIX) && + indexNameAndResp.getKey().contains(backingIndexName)) { + indexResp = indexNameAndResp.getValue(); + assertNotNull(indexResp); + assertThat(indexResp.getPhase(), equalTo("warm")); + assertThat(indexResp.getStep(), equalTo("complete")); + break; + } + } + + assertNotNull("Unable to find an ilm explain output for the shrunk index of " + index, indexResp); + }, 30, TimeUnit.SECONDS); } public void startHotOnlyNode() { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java index c026b803955f9..8ce5d2ab631e7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java @@ -110,23 +110,24 @@ public ClusterState execute(final ClusterState currentState) throws IOException policyStepsRegistry, false); } } else { - // set here to make sure that the clusterProcessed knows to execute the - // correct step if it an async action - nextStepKey = currentStep.getNextStepKey(); // cluster state wait step so evaluate the // condition, if the condition is met move to the // next step, if its not met return the current // cluster state so it can be applied and we will // wait for the next trigger to evaluate the // condition again - logger.trace("[{}] waiting for cluster state step condition ({}) [{}], next: [{}]", - index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), nextStepKey); + logger.trace("[{}] waiting for cluster state step condition ({}) [{}]", + index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey()); ClusterStateWaitStep.Result result; try { result = ((ClusterStateWaitStep) currentStep).isConditionMet(index, state); } catch (Exception exception) { return moveToErrorStep(state, currentStep.getKey(), exception); } + // some steps can decide to change the next step to execute after waiting for some time for the condition + // to be met (eg. {@link LifecycleSettings#LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING}, so it's important we + // re-evaluate what the next step is after we evaluate the condition + nextStepKey = currentStep.getNextStepKey(); if (result.isComplete()) { logger.trace("[{}] cluster state step condition met successfully ({}) [{}], moving to next step {}", index.getName(), currentStep.getClass().getSimpleName(), currentStep.getKey(), nextStepKey); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index faee630e648eb..7355145b83eb9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -179,6 +179,7 @@ public List> getSettings() { LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING, LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING, LifecycleSettings.LIFECYCLE_STEP_MASTER_TIMEOUT_SETTING, + LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING, RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING, LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING, LifecycleSettings.SLM_RETENTION_SCHEDULE_SETTING, diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index 79921a96ffb89..0e2b74f006b09 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -385,4 +385,9 @@ private boolean isClusterServiceStoppedOrClosed() { final State state = clusterService.lifecycleState(); return state == State.STOPPED || state == State.CLOSED; } + + // visible for testing + PolicyStepsRegistry getPolicyRegistry() { + return policyRegistry; + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportExplainLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportExplainLifecycleAction.java index 2978d0d91b104..fae250b69c42d 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportExplainLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportExplainLifecycleAction.java @@ -104,6 +104,7 @@ protected void doMasterOperation(ExplainLifecycleRequest request, String[] concr lifecycleState.getStepTime(), lifecycleState.getSnapshotRepository(), lifecycleState.getSnapshotName(), + lifecycleState.getShrinkIndexName(), stepInfoBytes, phaseExecutionInfo); } else { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java index f9b06ef7c7d92..a9202d24ae83a 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java @@ -290,7 +290,6 @@ public void testClusterWaitStepThrowsException() throws IOException { assertThat(currentStepKey, equalTo(new StepKey(firstStepKey.getPhase(), firstStepKey.getAction(), ErrorStep.NAME))); assertThat(firstStep.getExecuteCount(), equalTo(1L)); assertThat(secondStep.getExecuteCount(), equalTo(1L)); - assertThat(task.getNextStepKey(), equalTo(thirdStepKey)); assertThat(lifecycleState.getPhaseTime(), nullValue()); assertThat(lifecycleState.getActionTime(), nullValue()); assertThat(lifecycleState.getStepInfo(),