diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/AllocateAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/AllocateAction.java index cd46b1f91f72c..2e4ebbf4ac1ca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/AllocateAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/AllocateAction.java @@ -131,6 +131,13 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { return Arrays.asList(allocateStep, routedCheckStep); } + @Override + public List toStepKeys(String phase) { + StepKey allocateKey = new StepKey(phase, NAME, NAME); + StepKey allocationRoutedKey = new StepKey(phase, NAME, AllocationRoutedStep.NAME); + return Arrays.asList(allocateKey, allocationRoutedKey); + } + @Override public int hashCode() { return Objects.hash(include, exclude, require); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/DeleteAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/DeleteAction.java index c8337f9e1a6bd..1a0ad4c789ce4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/DeleteAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/DeleteAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; import java.io.IOException; import java.util.Collections; @@ -62,6 +63,11 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) return Collections.singletonList(new DeleteStep(deleteStepKey, nextStepKey, client)); } + @Override + public List toStepKeys(String phase) { + return Collections.singletonList(new Step.StepKey(phase, NAME, DeleteStep.NAME)); + } + @Override public int hashCode() { return 1; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ForceMergeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ForceMergeAction.java index 5fedb14ecffb4..1522d758f0ab6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ForceMergeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ForceMergeAction.java @@ -84,7 +84,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public List toSteps(Client client, String phase, Step.StepKey nextStepKey) { - StepKey updateCompressionKey = new StepKey(phase, NAME, "best_compression"); StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments); @@ -92,6 +91,13 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) return Arrays.asList(forceMergeStep, segmentCountStep); } + @Override + public List toStepKeys(String phase) { + StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME); + StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME); + return Arrays.asList(forceMergeKey, countKey); + } + @Override public int hashCode() { return Objects.hash(maxNumSegments); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleAction.java index d6ef78496bb4d..3e84813274d83 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; import java.util.List; @@ -29,6 +30,15 @@ public interface LifecycleAction extends ToXContentObject, NamedWriteable { */ List toSteps(Client client, String phase, @Nullable Step.StepKey nextStepKey); + /** + * + * @param phase + * the name of the phase this action is being executed within + * @return the {@link StepKey}s for the steps which will be executed in this + * action + */ + List toStepKeys(String phase); + /** * @return true if this action is considered safe. An action is not safe if * it will produce unwanted side effects or will get stuck when the diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java index fd8bf92a9c58d..84cb1bb418925 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicy.java @@ -245,6 +245,82 @@ public boolean isActionSafe(StepKey stepKey) { } } + /** + * Finds the next valid {@link StepKey} on or after the provided + * {@link StepKey}. If the provided {@link StepKey} is valid in this policy + * it will be returned. If its not valid the next available {@link StepKey} + * will be returned. + */ + public StepKey getNextValidStep(StepKey stepKey) { + Phase phase = phases.get(stepKey.getPhase()); + if (phase == null) { + // Phase doesn't exist so find the after step for the previous + // available phase + return getAfterStepBeforePhase(stepKey.getPhase()); + } else { + // Phase exists so check if the action exists + LifecycleAction action = phase.getActions().get(stepKey.getAction()); + if (action == null) { + // if action doesn't exist find the first step in the next + // available action + return getFirstStepInNextAction(stepKey.getAction(), phase); + } else { + // if the action exists check if the step itself exists + if (action.toStepKeys(phase.getName()).contains(stepKey)) { + // stepKey is valid still so return it + return stepKey; + } else { + // stepKey no longer exists in the action so we need to move + // to the first step in the next action since skipping steps + // in an action is not safe + return getFirstStepInNextAction(stepKey.getAction(), phase); + } + } + } + } + + private StepKey getNextAfterStep(String currentPhaseName) { + String nextPhaseName = type.getNextPhaseName(currentPhaseName, phases); + if (nextPhaseName == null) { + // We don't have a next phase after this one so there is no after + // step to move to. Instead we need to go to the terminal step as + // there are no more steps we should execute + return TerminalPolicyStep.KEY; + } else { + return new StepKey(currentPhaseName, PhaseAfterStep.NAME, PhaseAfterStep.NAME); + } + } + + private StepKey getAfterStepBeforePhase(String currentPhaseName) { + String nextPhaseName = type.getNextPhaseName(currentPhaseName, phases); + if (nextPhaseName == null) { + // We don't have a next phase after this one so the next step is the + // TerminalPolicyStep + return TerminalPolicyStep.KEY; + } else { + String prevPhaseName = type.getPreviousPhaseName(currentPhaseName, phases); + if (prevPhaseName == null) { + // no previous phase available so go to the + // InitializePolicyContextStep + return InitializePolicyContextStep.KEY; + } + return new StepKey(prevPhaseName, PhaseAfterStep.NAME, PhaseAfterStep.NAME); + } + } + + private StepKey getFirstStepInNextAction(String currentActionName, Phase phase) { + String nextActionName = type.getNextActionName(currentActionName, phase); + if (nextActionName == null) { + // The current action is the last in this phase so we need to find + // the next after step + return getNextAfterStep(phase.getName()); + } else { + LifecycleAction nextAction = phase.getActions().get(nextActionName); + // Return the first stepKey for nextAction + return nextAction.toStepKeys(phase.getName()).get(0); + } + } + @Override public int hashCode() { return Objects.hash(name, phases); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleType.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleType.java index 2c4c18199f82f..69be30fdfbd0e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleType.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/LifecycleType.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.io.stream.NamedWriteable; +import java.security.Policy; import java.util.Collection; import java.util.List; import java.util.Map; @@ -18,8 +19,47 @@ public interface LifecycleType extends NamedWriteable { */ List getOrderedPhases(Map phases); + /** + * Returns the next phase thats available after + * currentPhaseName. Note that currentPhaseName + * does not need to exist in phases. + * + * If the current {@link Phase} is the last phase in the {@link Policy} this + * method will return null. + * + * If the phase is not valid for the lifecycle type an + * {@link IllegalArgumentException} will be thrown. + */ + String getNextPhaseName(String currentPhaseName, Map phases); + + /** + * Returns the previous phase thats available before + * currentPhaseName. Note that currentPhaseName + * does not need to exist in phases. + * + * If the current {@link Phase} is the first phase in the {@link Policy} + * this method will return null. + * + * If the phase is not valid for the lifecycle type an + * {@link IllegalArgumentException} will be thrown. + */ + String getPreviousPhaseName(String currentPhaseName, Map phases); + List getOrderedActions(Phase phase); + /** + * Returns the name of the next phase that is available in the phases after + * currentActionName. Note that currentActionName + * does not need to exist in the {@link Phase}. + * + * If the current action is the last action in the phase this method will + * return null. + * + * If the action is not valid for the phase an + * {@link IllegalArgumentException} will be thrown. + */ + String getNextActionName(String currentActionName, Phase phase); + /** * validates whether the specified phases are valid for this diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReadOnlyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReadOnlyAction.java index ab081c6d6bd32..15edd51908bfe 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReadOnlyAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReadOnlyAction.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; import java.io.IOException; import java.util.Collections; @@ -65,6 +66,11 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) Settings readOnlySettings = Settings.builder().put(IndexMetaData.SETTING_BLOCKS_WRITE, true).build(); return Collections.singletonList(new UpdateSettingsStep(key, nextStepKey, client, readOnlySettings)); } + + @Override + public List toStepKeys(String phase) { + return Collections.singletonList(new Step.StepKey(phase, NAME, NAME)); + } @Override public int hashCode() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReplicasAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReplicasAction.java index 9bd0c767f6852..4d45f1aa89a11 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReplicasAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ReplicasAction.java @@ -87,6 +87,13 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) new ReplicasAllocatedStep(enoughKey, nextStepKey)); } + @Override + public List toStepKeys(String phase) { + StepKey updateReplicasKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME); + StepKey enoughKey = new StepKey(phase, NAME, ReplicasAllocatedStep.NAME); + return Arrays.asList(updateReplicasKey, enoughKey); + } + public int getNumberOfReplicas() { return numberOfReplicas; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/RolloverAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/RolloverAction.java index ff6dbcaf8be2c..78dce2db1b8c2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/RolloverAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/RolloverAction.java @@ -139,6 +139,13 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) return Arrays.asList(rolloverStep, updateDateStep); } + @Override + public List toStepKeys(String phase) { + StepKey rolloverStepKey = new StepKey(phase, NAME, RolloverStep.NAME); + StepKey updateDateStepKey = new StepKey(phase, NAME, UpdateRolloverLifecycleDateStep.NAME); + return Arrays.asList(rolloverStepKey, updateDateStepKey); + } + @Override public int hashCode() { return Objects.hash(maxSize, maxAge, maxDocs); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkAction.java index 190daabe70d4e..2e475b3d0170e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkAction.java @@ -97,6 +97,17 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) return Arrays.asList(setSingleNodeStep, allocationStep, shrink, allocated, aliasSwapAndDelete, waitOnShrinkTakeover); } + @Override + public List toStepKeys(String phase) { + StepKey setSingleNodeKey = new StepKey(phase, NAME, SetSingleNodeAllocateStep.NAME); + StepKey allocationRoutedKey = new StepKey(phase, NAME, AllocationRoutedStep.NAME); + StepKey shrinkKey = new StepKey(phase, NAME, ShrinkStep.NAME); + StepKey enoughShardsKey = new StepKey(phase, NAME, ShrunkShardsAllocatedStep.NAME); + StepKey aliasKey = new StepKey(phase, NAME, ShrinkSetAliasStep.NAME); + StepKey isShrunkIndexKey = new StepKey(phase, NAME, ShrunkenIndexCheckStep.NAME); + return Arrays.asList(setSingleNodeKey, allocationRoutedKey, shrinkKey, enoughShardsKey, aliasKey, isShrunkIndexKey); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java index 89056cac202e9..fb5fcf52956b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleType.java @@ -79,6 +79,48 @@ public List getOrderedPhases(Map phases) { return orderedPhases; } + @Override + public String getNextPhaseName(String currentPhaseName, Map phases) { + int index = VALID_PHASES.indexOf(currentPhaseName); + if (index < 0 && "new".equals(currentPhaseName) == false) { + throw new IllegalArgumentException("[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + TYPE + "]"); + } else { + // Find the next phase after `index` that exists in `phases` and return it + while (++index < VALID_PHASES.size()) { + String phaseName = VALID_PHASES.get(index); + if (phases.containsKey(phaseName)) { + return phaseName; + } + } + // if we have exhausted VALID_PHASES and haven't found a matching + // phase in `phases` return null indicating there is no next phase + // available + return null; + } + } + + public String getPreviousPhaseName(String currentPhaseName, Map phases) { + if ("new".equals(currentPhaseName)) { + return null; + } + int index = VALID_PHASES.indexOf(currentPhaseName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + TYPE + "]"); + } else { + // Find the previous phase before `index` that exists in `phases` and return it + while (--index >=0) { + String phaseName = VALID_PHASES.get(index); + if (phases.containsKey(phaseName)) { + return phaseName; + } + } + // if we have exhausted VALID_PHASES and haven't found a matching + // phase in `phases` return null indicating there is no previous phase + // available + return null; + } + } + public List getOrderedActions(Phase phase) { Map actions = phase.getActions(); switch (phase.getName()) { @@ -98,6 +140,45 @@ public List getOrderedActions(Phase phase) { throw new IllegalArgumentException("lifecycle type[" + TYPE + "] does not support phase[" + phase.getName() + "]"); } } + + @Override + public String getNextActionName(String currentActionName, Phase phase) { + List orderedActionNames; + switch (phase.getName()) { + case "hot": + orderedActionNames = ORDERED_VALID_HOT_ACTIONS; + break; + case "warm": + orderedActionNames = ORDERED_VALID_WARM_ACTIONS; + break; + case "cold": + orderedActionNames = ORDERED_VALID_COLD_ACTIONS; + break; + case "delete": + orderedActionNames = ORDERED_VALID_DELETE_ACTIONS; + break; + default: + throw new IllegalArgumentException("lifecycle type[" + TYPE + "] does not support phase[" + phase.getName() + "]"); + } + + int index = orderedActionNames.indexOf(currentActionName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentActionName + "] is not a valid action for phase [" + phase.getName() + + "] in lifecycle type [" + TYPE + "]"); + } else { + // Find the next action after `index` that exists in the phase and return it + while (++index < orderedActionNames.size()) { + String actionName = orderedActionNames.get(index); + if (phase.getActions().containsKey(actionName)) { + return actionName; + } + } + // if we have exhausted `validActions` and haven't found a matching + // action in the Phase return null indicating there is no next + // action available + return null; + } + } @Override public void validate(Collection phases) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/AbstractActionTestCase.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/AbstractActionTestCase.java index 12051e3d343a7..bed04a7cf5425 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/AbstractActionTestCase.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/AbstractActionTestCase.java @@ -7,6 +7,10 @@ package org.elasticsearch.xpack.core.indexlifecycle; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; + +import java.util.List; +import java.util.stream.Collectors; public abstract class AbstractActionTestCase extends AbstractSerializingTestCase { @@ -20,4 +24,17 @@ public final void testIsSafeAction() { LifecycleAction action = createTestInstance(); assertEquals(isSafeAction(), action.isSafeAction()); } + + public void testToStepKeys() { + T action = createTestInstance(); + String phase = randomAlphaOfLengthBetween(1, 10); + StepKey nextStepKey = new StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10), + randomAlphaOfLengthBetween(1, 10)); + List steps = action.toSteps(null, phase, nextStepKey); + assertNotNull(steps); + List stepKeys = action.toStepKeys(phase); + assertNotNull(stepKeys); + List expectedStepKeys = steps.stream().map(Step::getKey).collect(Collectors.toList()); + assertEquals(expectedStepKeys, stepKeys); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicyTests.java index d4552385f7839..134c69a528036 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/LifecyclePolicyTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -20,12 +21,15 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.LongSupplier; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -211,5 +215,284 @@ public void testIsActionSafe() { () -> policy.isActionSafe(new StepKey("first_phase", "non_existant_action", randomAlphaOfLength(10)))); assertEquals("Action [non_existant_action] in phase [first_phase] does not exist in policy [" + policy.getName() + "]", exception.getMessage()); + + assertTrue(policy.isActionSafe(new StepKey("new", randomAlphaOfLength(10), randomAlphaOfLength(10)))); + } + + public void testGetNextValidStep() { + List orderedPhases = Arrays.asList("phase_1", "phase_2", "phase_3", "phase_4", "phase_5"); + Map> orderedActionNamesForPhases = new HashMap<>(); + List actionNamesforPhase = new ArrayList<>(); + actionNamesforPhase.add("action_1"); + actionNamesforPhase.add("action_2"); + actionNamesforPhase.add("action_3"); + actionNamesforPhase.add("action_4"); + orderedActionNamesForPhases.put("phase_1", actionNamesforPhase); + orderedActionNamesForPhases.put("phase_2", actionNamesforPhase); + orderedActionNamesForPhases.put("phase_3", actionNamesforPhase); + orderedActionNamesForPhases.put("phase_4", actionNamesforPhase); + orderedActionNamesForPhases.put("phase_5", actionNamesforPhase); + LifecycleType lifecycleType = new ControllableLifecycleType(orderedPhases, orderedActionNamesForPhases); + + // create a policy which has only phases 1,2, and 4 and within them has + // actions 1 and 3 which both contain steps 1, 2, and 3. + Map phases = new HashMap<>(); + for (int p = 1; p <= 4; p++) { + if (p == 3) { + continue; + } + String phaseName = "phase_" + p; + Map actions = new HashMap<>(); + + for (int a = 1; a <= 3; a++) { + if (a == 2) { + continue; + } + String actionName = "action_" + a; + List steps = new ArrayList<>(); + for (int s = 1; s <= 3; s++) { + String stepName = "step_" + s; + steps.add(new MockStep(new StepKey(phaseName, actionName, stepName), null)); + } + NamedMockAction action = new NamedMockAction(actionName, steps); + actions.put(action.getWriteableName(), action); + } + + Phase phase = new Phase(phaseName, TimeValue.ZERO, actions); + phases.put(phase.getName(), phase); + } + LifecyclePolicy policy = new LifecyclePolicy(lifecycleType, lifecycleName, phases); + + // step still exists + StepKey currentStep = new StepKey(randomFrom("phase_1", "phase_2", "phase_4"), randomFrom("action_1", "action_3"), + randomFrom("step_1", "step_2", "step_3")); + StepKey nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(currentStep, nextStep); + + // current action exists but step does not + currentStep = new StepKey("phase_1", "action_1", "step_missing"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(new StepKey("phase_1", "action_3", "step_1"), nextStep); + + // current action exists but step does not and action is last in phase + currentStep = new StepKey("phase_1", "action_3", "step_missing"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(new StepKey("phase_1", PhaseAfterStep.NAME, PhaseAfterStep.NAME), nextStep); + + // current action exists but step does not and action is last in the + // last phase + currentStep = new StepKey("phase_4", "action_3", "step_missing"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(TerminalPolicyStep.KEY, nextStep); + + // current action no longer exists + currentStep = new StepKey("phase_1", "action_2", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(new StepKey("phase_1", "action_3", "step_1"), nextStep); + + // current action no longer exists and action was last in phase + currentStep = new StepKey("phase_1", "action_4", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(new StepKey("phase_1", PhaseAfterStep.NAME, PhaseAfterStep.NAME), nextStep); + + // current action no longer exists and action was last in the last phase + currentStep = new StepKey("phase_4", "action_4", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(TerminalPolicyStep.KEY, nextStep); + + // current phase no longer exists + currentStep = new StepKey("phase_3", "action_2", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(new StepKey("phase_2", PhaseAfterStep.NAME, PhaseAfterStep.NAME), nextStep); + + // current phase no longer exists and was last phase + currentStep = new StepKey("phase_5", "action_2", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(TerminalPolicyStep.KEY, nextStep); + + // create a new policy where only phase 2 exists and within it has + // actions 1 and 3 which both contain steps 1, 2, and 3. + phases = new HashMap<>(); + String phaseName = "phase_2"; + Map actions = new HashMap<>(); + + for (int a = 1; a <= 3; a++) { + if (a == 2) { + continue; + } + String actionName = "action_" + a; + List steps = new ArrayList<>(); + for (int s = 1; s <= 3; s++) { + String stepName = "step_" + s; + steps.add(new MockStep(new StepKey(phaseName, actionName, stepName), null)); + } + NamedMockAction action = new NamedMockAction(actionName, steps); + actions.put(action.getWriteableName(), action); + } + + Phase phase = new Phase(phaseName, TimeValue.ZERO, actions); + phases.put(phase.getName(), phase); + policy = new LifecyclePolicy(lifecycleType, lifecycleName, phases); + + // current phase no longer exists and was first phase + currentStep = new StepKey("phase_1", "action_2", "step_2"); + nextStep = policy.getNextValidStep(currentStep); + assertNotNull(nextStep); + assertEquals(InitializePolicyContextStep.KEY, nextStep); + + } + + private static class NamedMockAction extends MockAction { + + private final String name; + + NamedMockAction(String name, List steps) { + super(steps, true); + this.name = name; + } + + @Override + public String getWriteableName() { + return name; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException(); + } + } + + private static class ControllableLifecycleType implements LifecycleType { + + private final List orderedPhaseNames; + private final Map> orderedActionNamesForPhases; + + ControllableLifecycleType(List orderedPhases, Map> orderedActionNamesForPhases) { + this.orderedPhaseNames = orderedPhases; + this.orderedActionNamesForPhases = orderedActionNamesForPhases; + } + + @Override + public String getWriteableName() { + return "controllable_lifecycle_type"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + } + + @Override + public List getOrderedPhases(Map phases) { + return orderedPhaseNames.stream().map(n -> phases.get(n)).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public String getNextPhaseName(String currentPhaseName, Map phases) { + int index = orderedPhaseNames.indexOf(currentPhaseName); + if (index < 0) { + throw new IllegalArgumentException( + "[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + getWriteableName() + "]"); + } else if (index == orderedPhaseNames.size() - 1) { + return null; + } else { + // Find the next phase after `index` that exists in `phases` and return it + while (++index < orderedPhaseNames.size()) { + String phaseName = orderedPhaseNames.get(index); + if (phases.containsKey(phaseName)) { + return phaseName; + } + } + // if we have exhausted VALID_PHASES and haven't found a matching + // phase in `phases` return null indicating there is no next phase + // available + return null; + } + } + + @Override + public String getPreviousPhaseName(String currentPhaseName, Map phases) { + int index = orderedPhaseNames.indexOf(currentPhaseName); + if (index < 0) { + throw new IllegalArgumentException( + "[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + getWriteableName() + "]"); + } else if (index == orderedPhaseNames.size() - 1) { + return null; + } else { + // Find the previous phase before `index` that exists in `phases` and return it + while (--index >= 0) { + String phaseName = orderedPhaseNames.get(index); + if (phases.containsKey(phaseName)) { + return phaseName; + } + } + // if we have exhausted VALID_PHASES and haven't found a matching + // phase in `phases` return null indicating there is no next phase + // available + return null; + } + } + + @Override + public List getOrderedActions(Phase phase) { + List orderedActionNames = orderedActionNamesForPhases.get(phase.getName()); + if (orderedActionNames == null) { + throw new IllegalArgumentException( + "[" + phase.getName() + "] is not a valid phase for lifecycle type [" + getWriteableName() + "]"); + } + + return orderedActionNames.stream().map(n -> phase.getActions().get(n)).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public String getNextActionName(String currentActionName, Phase phase) { + List orderedActionNames = orderedActionNamesForPhases.get(phase.getName()); + if (orderedActionNames == null) { + throw new IllegalArgumentException( + "[" + phase.getName() + "] is not a valid phase for lifecycle type [" + getWriteableName() + "]"); + } + + int index = orderedActionNames.indexOf(currentActionName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentActionName + "] is not a valid action for phase [" + phase.getName() + + "] in lifecycle type [" + getWriteableName() + "]"); + } else { + // Find the next action after `index` that exists in the phase and return it + while (++index < orderedActionNames.size()) { + String actionName = orderedActionNames.get(index); + if (phase.getActions().containsKey(actionName)) { + return actionName; + } + } + // if we have exhausted `validActions` and haven't found a matching + // action in the Phase return null indicating there is no next + // action available + return null; + } + } + + @Override + public void validate(Collection phases) { + phases.forEach(phase -> { + if (orderedPhaseNames.contains(phase.getName()) == false) { + throw new IllegalArgumentException("Timeseries lifecycle does not support phase [" + phase.getName() + "]"); + } + List allowedActions = orderedActionNamesForPhases.get(phase.getName()); + phase.getActions().forEach((actionName, action) -> { + if (allowedActions.contains(actionName) == false) { + throw new IllegalArgumentException( + "invalid action [" + actionName + "] " + "defined in phase [" + phase.getName() + "]"); + } + }); + }); + } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/MockAction.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/MockAction.java index f978593167d54..30eabac562606 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/MockAction.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/MockAction.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; import java.io.IOException; import java.util.ArrayList; @@ -74,6 +75,11 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) return new ArrayList<>(steps); } + @Override + public List toStepKeys(String phase) { + return steps.stream().map(Step::getKey).collect(Collectors.toList()); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeList(steps.stream().map(MockStep::new).collect(Collectors.toList())); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TestLifecycleType.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TestLifecycleType.java index 1ca6de65ee7cb..f68798e9331d1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TestLifecycleType.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TestLifecycleType.java @@ -12,6 +12,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class TestLifecycleType implements LifecycleType { public static final TestLifecycleType INSTANCE = new TestLifecycleType(); @@ -40,8 +41,49 @@ public List getOrderedPhases(Map phases) { return new ArrayList<>(phases.values()); } + @Override + public String getNextPhaseName(String currentPhaseName, Map phases) { + List orderedPhaseNames = getOrderedPhases(phases).stream().map(Phase::getName).collect(Collectors.toList()); + int index = orderedPhaseNames.indexOf(currentPhaseName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + TYPE + "]"); + } else if (index == orderedPhaseNames.size() - 1) { + return null; + } else { + return orderedPhaseNames.get(index + 1); + } + } + + @Override + public String getPreviousPhaseName(String currentPhaseName, Map phases) { + List orderedPhaseNames = getOrderedPhases(phases).stream().map(Phase::getName).collect(Collectors.toList()); + int index = orderedPhaseNames.indexOf(currentPhaseName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentPhaseName + "] is not a valid phase for lifecycle type [" + TYPE + "]"); + } else if (index == 0) { + return null; + } else { + return orderedPhaseNames.get(index - 1); + } + } + @Override public List getOrderedActions(Phase phase) { return new ArrayList<>(phase.getActions().values()); } + + @Override + public String getNextActionName(String currentActionName, Phase phase) { + List orderedActionNames = getOrderedActions(phase).stream().map(LifecycleAction::getWriteableName) + .collect(Collectors.toList()); + int index = orderedActionNames.indexOf(currentActionName); + if (index < 0) { + throw new IllegalArgumentException("[" + currentActionName + "] is not a valid action for phase [" + phase.getName() + + "] in lifecycle type [" + TYPE + "]"); + } else if (index == orderedActionNames.size() - 1) { + return null; + } else { + return orderedActionNames.get(index + 1); + } + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleTypeTests.java index c54efd38fcd8c..d1af22172a4b0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/TimeseriesLifecycleTypeTests.java @@ -9,10 +9,12 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.stream.Collectors; @@ -186,6 +188,280 @@ public void testGetOrderedActionsDelete() { assertTrue(isSorted(orderedActions, LifecycleAction::getWriteableName, ORDERED_VALID_DELETE_ACTIONS)); } + public void testGetNextPhaseName() { + assertNextPhaseName("hot", "warm", new String[] { "hot", "warm" }); + assertNextPhaseName("hot", "warm", new String[] { "hot", "warm", "cold" }); + assertNextPhaseName("hot", "warm", new String[] { "hot", "warm", "cold", "delete" }); + assertNextPhaseName("hot", "warm", new String[] { "warm", "cold", "delete" }); + assertNextPhaseName("hot", "warm", new String[] { "warm", "cold", "delete" }); + assertNextPhaseName("hot", "warm", new String[] { "warm", "delete" }); + assertNextPhaseName("hot", "cold", new String[] { "cold", "delete" }); + assertNextPhaseName("hot", "cold", new String[] { "cold" }); + assertNextPhaseName("hot", "delete", new String[] { "hot", "delete" }); + assertNextPhaseName("hot", "delete", new String[] { "delete" }); + assertNextPhaseName("hot", null, new String[] { "hot" }); + assertNextPhaseName("hot", null, new String[] {}); + + assertNextPhaseName("warm", "cold", new String[] { "hot", "warm", "cold", "delete" }); + assertNextPhaseName("warm", "cold", new String[] { "warm", "cold", "delete" }); + assertNextPhaseName("warm", "cold", new String[] { "cold", "delete" }); + assertNextPhaseName("warm", "cold", new String[] { "cold" }); + assertNextPhaseName("warm", "delete", new String[] { "hot", "warm", "delete" }); + assertNextPhaseName("warm", null, new String[] { "hot", "warm" }); + assertNextPhaseName("warm", null, new String[] { "warm" }); + assertNextPhaseName("warm", null, new String[] { "hot" }); + assertNextPhaseName("warm", null, new String[] {}); + + assertNextPhaseName("cold", "delete", new String[] { "hot", "warm", "cold", "delete" }); + assertNextPhaseName("cold", "delete", new String[] { "warm", "cold", "delete" }); + assertNextPhaseName("cold", "delete", new String[] { "cold", "delete" }); + assertNextPhaseName("cold", "delete", new String[] { "delete" }); + assertNextPhaseName("cold", "delete", new String[] { "hot", "warm", "delete" }); + assertNextPhaseName("cold", null, new String[] { "hot", "warm", "cold" }); + assertNextPhaseName("cold", null, new String[] { "hot", "warm" }); + assertNextPhaseName("cold", null, new String[] { "cold" }); + assertNextPhaseName("cold", null, new String[] { "hot" }); + assertNextPhaseName("cold", null, new String[] {}); + + assertNextPhaseName("delete", null, new String[] { "hot", "warm", "cold" }); + assertNextPhaseName("delete", null, new String[] { "hot", "warm" }); + assertNextPhaseName("delete", null, new String[] { "cold" }); + assertNextPhaseName("delete", null, new String[] { "hot" }); + assertNextPhaseName("delete", null, new String[] {}); + assertNextPhaseName("delete", null, new String[] { "hot", "warm", "cold", "delete" }); + assertNextPhaseName("delete", null, new String[] { "hot", "warm", "delete" }); + assertNextPhaseName("delete", null, new String[] { "cold", "delete" }); + assertNextPhaseName("delete", null, new String[] { "delete" }); + assertNextPhaseName("delete", null, new String[] {}); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> TimeseriesLifecycleType.INSTANCE.getNextPhaseName("foo", Collections.emptyMap())); + assertEquals("[foo] is not a valid phase for lifecycle type [" + TimeseriesLifecycleType.TYPE + "]", exception.getMessage()); + exception = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE + .getNextPhaseName("foo", Collections.singletonMap("foo", new Phase("foo", TimeValue.ZERO, Collections.emptyMap())))); + assertEquals("[foo] is not a valid phase for lifecycle type [" + TimeseriesLifecycleType.TYPE + "]", exception.getMessage()); + } + + public void testGetPreviousPhaseName() { + assertPreviousPhaseName("hot", null, new String[] { "hot", "warm" }); + assertPreviousPhaseName("hot", null, new String[] { "hot", "warm", "cold" }); + assertPreviousPhaseName("hot", null, new String[] { "hot", "warm", "cold", "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "warm", "cold", "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "warm", "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "cold", "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "cold" }); + assertPreviousPhaseName("hot", null, new String[] { "hot", "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "delete" }); + assertPreviousPhaseName("hot", null, new String[] { "hot" }); + assertPreviousPhaseName("hot", null, new String[] {}); + + assertPreviousPhaseName("warm", "hot", new String[] { "hot", "warm", "cold", "delete" }); + assertPreviousPhaseName("warm", null, new String[] { "warm", "cold", "delete" }); + assertPreviousPhaseName("warm", "hot", new String[] { "hot", "cold", "delete" }); + assertPreviousPhaseName("warm", null, new String[] { "cold", "delete" }); + assertPreviousPhaseName("warm", "hot", new String[] { "hot", "delete" }); + assertPreviousPhaseName("warm", null, new String[] { "delete" }); + assertPreviousPhaseName("warm", "hot", new String[] { "hot" }); + assertPreviousPhaseName("warm", null, new String[] {}); + + assertPreviousPhaseName("cold", "warm", new String[] { "hot", "warm", "cold", "delete" }); + assertPreviousPhaseName("cold", "hot", new String[] { "hot", "cold", "delete" }); + assertPreviousPhaseName("cold", "warm", new String[] { "warm", "cold", "delete" }); + assertPreviousPhaseName("cold", null, new String[] { "cold", "delete" }); + assertPreviousPhaseName("cold", "warm", new String[] { "hot", "warm", "delete" }); + assertPreviousPhaseName("cold", "hot", new String[] { "hot", "delete" }); + assertPreviousPhaseName("cold", "warm", new String[] { "warm", "delete" }); + assertPreviousPhaseName("cold", null, new String[] { "delete" }); + assertPreviousPhaseName("cold", "warm", new String[] { "hot", "warm" }); + assertPreviousPhaseName("cold", "hot", new String[] { "hot" }); + assertPreviousPhaseName("cold", "warm", new String[] { "warm" }); + assertPreviousPhaseName("cold", null, new String[] {}); + + assertPreviousPhaseName("delete", "cold", new String[] { "hot", "warm", "cold", "delete" }); + assertPreviousPhaseName("delete", "cold", new String[] { "warm", "cold", "delete" }); + assertPreviousPhaseName("delete", "warm", new String[] { "hot", "warm", "delete" }); + assertPreviousPhaseName("delete", "hot", new String[] { "hot", "delete" }); + assertPreviousPhaseName("delete", "cold", new String[] { "cold", "delete" }); + assertPreviousPhaseName("delete", null, new String[] { "delete" }); + assertPreviousPhaseName("delete", "cold", new String[] { "hot", "warm", "cold" }); + assertPreviousPhaseName("delete", "cold", new String[] { "warm", "cold" }); + assertPreviousPhaseName("delete", "warm", new String[] { "hot", "warm" }); + assertPreviousPhaseName("delete", "hot", new String[] { "hot" }); + assertPreviousPhaseName("delete", "cold", new String[] { "cold" }); + assertPreviousPhaseName("delete", null, new String[] {}); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> TimeseriesLifecycleType.INSTANCE.getPreviousPhaseName("foo", Collections.emptyMap())); + assertEquals("[foo] is not a valid phase for lifecycle type [" + TimeseriesLifecycleType.TYPE + "]", exception.getMessage()); + exception = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE + .getPreviousPhaseName("foo", Collections.singletonMap("foo", new Phase("foo", TimeValue.ZERO, Collections.emptyMap())))); + assertEquals("[foo] is not a valid phase for lifecycle type [" + TimeseriesLifecycleType.TYPE + "]", exception.getMessage()); + } + + public void testGetNextActionName() { + // Hot Phase + assertNextActionName("hot", RolloverAction.NAME, null, new String[] {}); + assertNextActionName("hot", RolloverAction.NAME, null, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", "foo", new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", AllocateAction.NAME, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", DeleteAction.NAME, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", ForceMergeAction.NAME, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", ReadOnlyAction.NAME, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", ReplicasAction.NAME, new String[] { RolloverAction.NAME }); + assertInvalidAction("hot", ShrinkAction.NAME, new String[] { RolloverAction.NAME }); + + // Warm Phase + assertNextActionName("warm", ReadOnlyAction.NAME, AllocateAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ReplicasAction.NAME, + new String[] { ReadOnlyAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ShrinkAction.NAME, + new String[] { ReadOnlyAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ForceMergeAction.NAME, + new String[] { ReadOnlyAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, null, new String[] { ReadOnlyAction.NAME }); + + assertNextActionName("warm", ReadOnlyAction.NAME, AllocateAction.NAME, + new String[] { AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ReplicasAction.NAME, + new String[] { ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ShrinkAction.NAME, new String[] { ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, ForceMergeAction.NAME, new String[] { ForceMergeAction.NAME }); + assertNextActionName("warm", ReadOnlyAction.NAME, null, new String[] {}); + + assertNextActionName("warm", AllocateAction.NAME, ReplicasAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, ShrinkAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, ForceMergeAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, null, new String[] { ReadOnlyAction.NAME, AllocateAction.NAME }); + + assertNextActionName("warm", AllocateAction.NAME, ReplicasAction.NAME, + new String[] { ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, ShrinkAction.NAME, new String[] { ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, ForceMergeAction.NAME, new String[] { ForceMergeAction.NAME }); + assertNextActionName("warm", AllocateAction.NAME, null, new String[] {}); + + assertNextActionName("warm", ReplicasAction.NAME, ShrinkAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReplicasAction.NAME, ForceMergeAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReplicasAction.NAME, null, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME }); + + assertNextActionName("warm", ReplicasAction.NAME, ShrinkAction.NAME, new String[] { ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ReplicasAction.NAME, ForceMergeAction.NAME, new String[] { ForceMergeAction.NAME }); + assertNextActionName("warm", ReplicasAction.NAME, null, new String[] {}); + + assertNextActionName("warm", ShrinkAction.NAME, ForceMergeAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertNextActionName("warm", ShrinkAction.NAME, null, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME }); + + assertNextActionName("warm", ShrinkAction.NAME, ForceMergeAction.NAME, new String[] { ForceMergeAction.NAME }); + assertNextActionName("warm", ShrinkAction.NAME, null, new String[] {}); + + assertNextActionName("warm", ForceMergeAction.NAME, null, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + + assertNextActionName("warm", ForceMergeAction.NAME, null, new String[] {}); + + assertInvalidAction("warm", "foo", new String[] { RolloverAction.NAME }); + assertInvalidAction("warm", DeleteAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + assertInvalidAction("warm", RolloverAction.NAME, + new String[] { ReadOnlyAction.NAME, AllocateAction.NAME, ReplicasAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME }); + + // Cold Phase + assertNextActionName("cold", AllocateAction.NAME, ReplicasAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertNextActionName("cold", AllocateAction.NAME, null, new String[] { AllocateAction.NAME }); + + assertNextActionName("cold", AllocateAction.NAME, ReplicasAction.NAME, new String[] { ReplicasAction.NAME }); + assertNextActionName("cold", AllocateAction.NAME, null, new String[] {}); + + assertNextActionName("cold", ReplicasAction.NAME, null, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertNextActionName("cold", AllocateAction.NAME, null, new String[] {}); + + assertInvalidAction("cold", "foo", new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertInvalidAction("cold", DeleteAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertInvalidAction("cold", ForceMergeAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertInvalidAction("cold", ReadOnlyAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertInvalidAction("cold", RolloverAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + assertInvalidAction("cold", ShrinkAction.NAME, new String[] { AllocateAction.NAME, ReplicasAction.NAME }); + + // Delete Phase + assertNextActionName("delete", DeleteAction.NAME, null, new String[] {}); + assertNextActionName("delete", DeleteAction.NAME, null, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", "foo", new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", AllocateAction.NAME, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", ForceMergeAction.NAME, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", ReadOnlyAction.NAME, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", ReplicasAction.NAME, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", RolloverAction.NAME, new String[] { DeleteAction.NAME }); + assertInvalidAction("delete", ShrinkAction.NAME, new String[] { DeleteAction.NAME }); + + Phase phase = new Phase("foo", TimeValue.ZERO, Collections.emptyMap()); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> TimeseriesLifecycleType.INSTANCE.getNextActionName(ShrinkAction.NAME, phase)); + assertEquals("lifecycle type[" + TimeseriesLifecycleType.TYPE + "] does not support phase[" + phase.getName() + "]", + exception.getMessage()); + } + + private void assertNextActionName(String phaseName, String currentAction, String expectedNextAction, String... availableActionNames) { + Map availableActions = convertActionNamesToActions(availableActionNames); + Phase phase = new Phase(phaseName, TimeValue.ZERO, availableActions); + String nextAction = TimeseriesLifecycleType.INSTANCE.getNextActionName(currentAction, phase); + assertEquals(expectedNextAction, nextAction); + } + + private void assertInvalidAction(String phaseName, String currentAction, String... availableActionNames) { + Map availableActions = convertActionNamesToActions(availableActionNames); + Phase phase = new Phase(phaseName, TimeValue.ZERO, availableActions); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, + () -> TimeseriesLifecycleType.INSTANCE.getNextActionName(currentAction, phase)); + assertEquals("[" + currentAction + "] is not a valid action for phase [" + phaseName + "] in lifecycle type [" + + TimeseriesLifecycleType.TYPE + "]", exception.getMessage()); + } + + private ConcurrentMap convertActionNamesToActions(String... availableActionNames) { + return Arrays.asList(availableActionNames).stream().map(n -> { + switch (n) { + case AllocateAction.NAME: + return new AllocateAction(Collections.singletonMap("foo", "bar"), Collections.emptyMap(), Collections.emptyMap()); + case DeleteAction.NAME: + return new DeleteAction(); + case ForceMergeAction.NAME: + return new ForceMergeAction(1); + case ReadOnlyAction.NAME: + return new ReadOnlyAction(); + case ReplicasAction.NAME: + return new ReplicasAction(1); + case RolloverAction.NAME: + return new RolloverAction(ByteSizeValue.parseBytesSizeValue("0b", "test"), TimeValue.ZERO, 1L); + case ShrinkAction.NAME: + return new ShrinkAction(1); + } + return new DeleteAction(); + }).collect(Collectors.toConcurrentMap(LifecycleAction::getWriteableName, Function.identity())); + } + + private void assertNextPhaseName(String currentPhase, String expectedNextPhase, String... availablePhaseNames) { + Map availablePhases = Arrays.asList(availablePhaseNames).stream() + .map(n -> new Phase(n, TimeValue.ZERO, Collections.emptyMap())) + .collect(Collectors.toMap(Phase::getName, Function.identity())); + String nextPhase = TimeseriesLifecycleType.INSTANCE.getNextPhaseName(currentPhase, availablePhases); + assertEquals(expectedNextPhase, nextPhase); + } + + private void assertPreviousPhaseName(String currentPhase, String expectedNextPhase, String... availablePhaseNames) { + Map availablePhases = Arrays.asList(availablePhaseNames).stream() + .map(n -> new Phase(n, TimeValue.ZERO, Collections.emptyMap())) + .collect(Collectors.toMap(Phase::getName, Function.identity())); + String nextPhase = TimeseriesLifecycleType.INSTANCE.getPreviousPhaseName(currentPhase, availablePhases); + assertEquals(expectedNextPhase, nextPhase); + } + /** * checks whether an ordered list of objects (usually Phase and LifecycleAction) are found in the same * order as the ordered VALID_PHASES/VALID_HOT_ACTIONS/... lists diff --git a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunner.java b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunner.java index 6d7f732b89ca3..c5de1454f500a 100644 --- a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunner.java +++ b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunner.java @@ -288,7 +288,7 @@ private void setStepInfo(Index index, String policy, StepKey currentStepKey, ToX } public static ClusterState setPolicyForIndexes(final String newPolicyName, final Index[] indices, ClusterState currentState, - LifecyclePolicy newPolicy, List failedIndexes) { + LifecyclePolicy newPolicy, List failedIndexes, LongSupplier nowSupplier) { Map policiesMap = ((IndexLifecycleMetadata) currentState.metaData().custom(IndexLifecycleMetadata.TYPE)) .getPolicies(); MetaData.Builder newMetadata = MetaData.builder(currentState.getMetaData()); @@ -300,7 +300,7 @@ public static ClusterState setPolicyForIndexes(final String newPolicyName, final failedIndexes.add(index.getName()); } else { IndexMetaData.Builder newIdxMetadata = IndexLifecycleRunner.setPolicyForIndex(newPolicyName, newPolicy, policiesMap, - failedIndexes, index, indexMetadata); + failedIndexes, index, indexMetadata, nowSupplier); if (newIdxMetadata != null) { newMetadata.put(newIdxMetadata); clusterStateChanged = true; @@ -317,9 +317,9 @@ public static ClusterState setPolicyForIndexes(final String newPolicyName, final } private static IndexMetaData.Builder setPolicyForIndex(final String newPolicyName, LifecyclePolicy newPolicy, - Map policiesMap, List failedIndexes, Index index, IndexMetaData indexMetadata) { + Map policiesMap, List failedIndexes, Index index, IndexMetaData indexMetadata, + LongSupplier nowSupplier) { Settings idxSettings = indexMetadata.getSettings(); - Settings.Builder newSettings = Settings.builder().put(idxSettings); String currentPolicyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(idxSettings); StepKey currentStepKey = IndexLifecycleRunner.getCurrentStepKey(idxSettings); LifecyclePolicy currentPolicy = null; @@ -328,8 +328,16 @@ private static IndexMetaData.Builder setPolicyForIndex(final String newPolicyNam } if (canSetPolicy(currentStepKey, currentPolicy, newPolicy)) { + Settings.Builder newSettings = Settings.builder().put(idxSettings); + if (currentStepKey != null) { + // Check if current step exists in new policy and if not move to + // next available step + StepKey nextValidStepKey = newPolicy.getNextValidStep(currentStepKey); + if (nextValidStepKey.equals(currentStepKey) == false) { + newSettings = moveIndexSettingsToNextStep(idxSettings, currentStepKey, nextValidStepKey, nowSupplier); + } + } newSettings.put(LifecycleSettings.LIFECYCLE_NAME_SETTING.getKey(), newPolicyName); - // NORELEASE check if current step exists in new policy and if not move to next available step return IndexMetaData.builder(indexMetadata).settings(newSettings); } else { failedIndexes.add(index.getName()); diff --git a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportPutLifecycleAction.java b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportPutLifecycleAction.java index b8bce5aa29b75..930777f1f4fc8 100644 --- a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportPutLifecycleAction.java @@ -78,6 +78,7 @@ public ClusterState execute(ClusterState currentState) throws Exception { throw new ResourceAlreadyExistsException("Lifecycle policy already exists: {}", request.getPolicy().getName()); } + // NORELEASE Check if current step exists in new policy and if not move to next available step SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); Map filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream() diff --git a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportSetPolicyForIndexAction.java b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportSetPolicyForIndexAction.java index cf5d976e6f704..4e66e77d5a953 100644 --- a/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportSetPolicyForIndexAction.java +++ b/x-pack/plugin/index-lifecycle/src/main/java/org/elasticsearch/xpack/indexlifecycle/action/TransportSetPolicyForIndexAction.java @@ -79,7 +79,8 @@ public ClusterState execute(ClusterState currentState) throws Exception { throw new ResourceNotFoundException("Policy does not exist [{}]", newPolicyName); } - return IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, currentState, newPolicy, failedIndexes); + return IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, currentState, newPolicy, failedIndexes, + () -> System.currentTimeMillis()); } @Override diff --git a/x-pack/plugin/index-lifecycle/src/test/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunnerTests.java b/x-pack/plugin/index-lifecycle/src/test/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunnerTests.java index 5daec7cd66900..e0a4c93e74f97 100644 --- a/x-pack/plugin/index-lifecycle/src/test/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunnerTests.java +++ b/x-pack/plugin/index-lifecycle/src/test/java/org/elasticsearch/xpack/indexlifecycle/IndexLifecycleRunnerTests.java @@ -846,8 +846,10 @@ public void testSetPolicyForIndex() { String indexName = randomAlphaOfLength(10); String oldPolicyName = "old_policy"; String newPolicyName = "new_policy"; - LifecyclePolicy newPolicy = new LifecyclePolicy(TestLifecycleType.INSTANCE, newPolicyName, Collections.emptyMap()); - StepKey currentStep = new StepKey(randomAlphaOfLength(10), MockAction.NAME, randomAlphaOfLength(10)); + String phaseName = randomAlphaOfLength(10); + StepKey currentStep = new StepKey(phaseName, MockAction.NAME, randomAlphaOfLength(10)); + LifecyclePolicy newPolicy = createPolicy(oldPolicyName, + new StepKey(phaseName, MockAction.NAME, randomAlphaOfLength(9)), null); LifecyclePolicy oldPolicy = createPolicy(oldPolicyName, currentStep, null); Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName) .put(LifecycleSettings.LIFECYCLE_PHASE, currentStep.getPhase()) @@ -861,10 +863,10 @@ public void testSetPolicyForIndex() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertTrue(failedIndexes.isEmpty()); - assertClusterStateOnPolicy(clusterState, index, newPolicyName, currentStep, currentStep, newClusterState, now); + assertClusterStateOnPolicy(clusterState, index, newPolicyName, currentStep, TerminalPolicyStep.KEY, newClusterState, now); } public void testSetPolicyForIndexNoCurrentPolicy() { @@ -880,13 +882,14 @@ public void testSetPolicyForIndexNoCurrentPolicy() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertTrue(failedIndexes.isEmpty()); assertClusterStateOnPolicy(clusterState, index, newPolicyName, currentStep, currentStep, newClusterState, now); } public void testSetPolicyForIndexIndexDoesntExist() { + long now = randomNonNegativeLong(); String indexName = randomAlphaOfLength(10); String oldPolicyName = "old_policy"; String newPolicyName = "new_policy"; @@ -905,7 +908,7 @@ public void testSetPolicyForIndexIndexDoesntExist() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertEquals(1, failedIndexes.size()); assertEquals("doesnt_exist", failedIndexes.get(0)); @@ -913,6 +916,7 @@ public void testSetPolicyForIndexIndexDoesntExist() { } public void testSetPolicyForIndexIndexInUnsafe() { + long now = randomNonNegativeLong(); String indexName = randomAlphaOfLength(10); String oldPolicyName = "old_policy"; String newPolicyName = "new_policy"; @@ -931,7 +935,7 @@ public void testSetPolicyForIndexIndexInUnsafe() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertEquals(1, failedIndexes.size()); assertEquals(index.getName(), failedIndexes.get(0)); @@ -958,13 +962,14 @@ public void testSetPolicyForIndexIndexInUnsafeActionUnchanged() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertTrue(failedIndexes.isEmpty()); assertClusterStateOnPolicy(clusterState, index, newPolicyName, currentStep, currentStep, newClusterState, now); } public void testSetPolicyForIndexIndexInUnsafeActionChanged() { + long now = randomNonNegativeLong(); String indexName = randomAlphaOfLength(10); String oldPolicyName = "old_policy"; String newPolicyName = "new_policy"; @@ -974,7 +979,10 @@ public void testSetPolicyForIndexIndexInUnsafeActionChanged() { // change the current action so its not equal to the old one by adding a step Map phases = new HashMap<>(); Map actions = new HashMap<>(); - MockAction unsafeAction = new MockAction(Collections.singletonList(new MockStep(currentStep, null)), false); + List steps = new ArrayList<>(); + steps.add(new MockStep(currentStep, null)); + steps.add(new MockStep(new StepKey(currentStep.getPhase(), currentStep.getAction(), randomAlphaOfLength(5)), null)); + MockAction unsafeAction = new MockAction(steps, false); actions.put(unsafeAction.getWriteableName(), unsafeAction); Phase phase = new Phase(currentStep.getPhase(), TimeValue.timeValueMillis(0), actions); phases.put(phase.getName(), phase); @@ -992,7 +1000,7 @@ public void testSetPolicyForIndexIndexInUnsafeActionChanged() { List failedIndexes = new ArrayList<>(); ClusterState newClusterState = IndexLifecycleRunner.setPolicyForIndexes(newPolicyName, indices, clusterState, newPolicy, - failedIndexes); + failedIndexes, () -> now); assertEquals(1, failedIndexes.size()); assertEquals(index.getName(), failedIndexes.get(0)); @@ -1006,7 +1014,8 @@ private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, assert unsafeStep == null || safeStep.getPhase().equals(unsafeStep.getPhase()) == false : "safe and unsafe actions must be in different phases"; Map actions = new HashMap<>(); - MockAction safeAction = new MockAction(Collections.emptyList(), true); + List steps = Collections.singletonList(new MockStep(safeStep, null)); + MockAction safeAction = new MockAction(steps, true); actions.put(safeAction.getWriteableName(), safeAction); Phase phase = new Phase(safeStep.getPhase(), TimeValue.timeValueMillis(0), actions); phases.put(phase.getName(), phase); @@ -1014,7 +1023,8 @@ private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, if (unsafeStep != null) { assert MockAction.NAME.equals(unsafeStep.getAction()) : "The unsafe action needs to be MockAction.NAME"; Map actions = new HashMap<>(); - MockAction unsafeAction = new MockAction(Collections.emptyList(), false); + List steps = Collections.singletonList(new MockStep(unsafeStep, null)); + MockAction unsafeAction = new MockAction(steps, false); actions.put(unsafeAction.getWriteableName(), unsafeAction); Phase phase = new Phase(unsafeStep.getPhase(), TimeValue.timeValueMillis(0), actions); phases.put(phase.getName(), phase); @@ -1094,7 +1104,10 @@ public void testCanUpdatePolicyIndexInUnsafeActionChanged() { // change the current action so its not equal to the old one by adding a step Map phases = new HashMap<>(); Map actions = new HashMap<>(); - MockAction unsafeAction = new MockAction(Collections.singletonList(new MockStep(currentStep, null)), false); + List newSteps = new ArrayList<>(); + newSteps.add(new MockStep(currentStep, null)); + newSteps.add(new MockStep(new StepKey(currentStep.getPhase(), currentStep.getAction(), randomAlphaOfLength(5)), null)); + MockAction unsafeAction = new MockAction(newSteps, false); actions.put(unsafeAction.getWriteableName(), unsafeAction); Phase phase = new Phase(currentStep.getPhase(), TimeValue.timeValueMillis(0), actions); phases.put(phase.getName(), phase);