From 0b3e3a083c60bfa727e0d11352c9a0c1c26e898c Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 28 May 2021 13:27:25 +0100 Subject: [PATCH 01/19] Service to migrate indices and ILM policies to data tiers This adds a service that migrates the indices and ILM policies away from custom node attribute allocation routing to data tiers. --- ...adataMigrateToDataTiersRoutingService.java | 280 ++++++++++++ .../elasticsearch/xpack/core/DataTier.java | 19 + .../xpack/core/ilm/MigrateAction.java | 19 +- .../xpack/core/ilm/PhaseCacheManagement.java | 242 +++++++++++ ...MigrateToDataTiersRoutingServiceTests.java | 410 ++++++++++++++++++ .../xpack/core/DataTierTests.java | 13 + .../xpack/core/ilm/MigrateActionTests.java | 9 - .../core/ilm/PhaseCacheManagementTests.java} | 299 +++++++------ .../action/TransportPutLifecycleAction.java | 187 +------- .../ilm/IndexLifecycleTransitionTests.java | 143 +++++- 10 files changed, 1274 insertions(+), 347 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java rename x-pack/plugin/{ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java => core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java} (73%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java new file mode 100644 index 0000000000000..aaf98ced263b6 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -0,0 +1,280 @@ +/* + * 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.cluster.metadata; + +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.xpack.core.DataTier; +import org.elasticsearch.xpack.core.ilm.AllocateAction; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +import org.elasticsearch.xpack.core.ilm.MigrateAction; +import org.elasticsearch.xpack.core.ilm.Phase; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; + +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; +import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; + +/** + * Exposes the necessary methods to migrate a system's elasticsearch abstractions to use data tiers for index allocation routing. + */ +public final class MetadataMigrateToDataTiersRoutingService { + + public static final String DEFAULT_NODE_ATTRIBUTE_NAME = "data"; + private static final Logger logger = LogManager.getLogger(MetadataMigrateToDataTiersRoutingService.class); + + private MetadataMigrateToDataTiersRoutingService() { + } + + /** + * Migrates the elasticsearch abstractions to use data tiers for allocation routing. + * This will: + * - remove the given V1 index template if it exists. + * - loop through the existing ILM policies and look at the configured {@link AllocateAction}s. If they define *any* routing rules + * based on the provided node attribute name (we look at include, exclude and require rules) *ALL* the rules in the allocate action + * will be removed. All the rules are removed in order to allow for ILM to inject the {@link MigrateAction}. + * So for eg. this action: + * allocate { + * number_of_replicas: 0, + * require: {data: warm}, + * include: {rack: one} + * } + * will become + * allocate { + * number_of_replicas: 0 + * } + * Note that if the `allocate` action doesn't define any `number_of_replicas` it will be removed completely from the migrated policy. + * + * - loop through all the indices convert the index.routing.allocation.require.{nodeAttrName} setting (if present) to the + * corresponding data tier `_tier_preference` routing. We are only able to convert the `frozen`, `cold`, `warm`, or `hot` setting + * values to the `_tier_preference`. If other configuration values are present eg ("the_warm_nodes") the index will not be migrated. + * + * If no @param nodeAttrName is provided "data" will be used. + * If no @param indexTemplateToDelete is provided, no index templates will be deleted. + * + * This returns a new {@link ClusterState} representing the migrated state that is ready to use data tiers for index and + * ILM routing allocations. It also returns a summary of the affected abstractions encapsulated in {@link MigratedEntities} + */ + public static Tuple migrateToDataTiersRouting(ClusterState currentState, + @Nullable String nodeAttrName, + @Nullable String indexTemplateToDelete, + NamedXContentRegistry xContentRegistry, Client client) { + Metadata.Builder mb = Metadata.builder(currentState.metadata()); + + String removedIndexTemplateName = null; + if (Strings.isNullOrEmpty(indexTemplateToDelete) == false && + currentState.metadata().getTemplates().containsKey(indexTemplateToDelete)) { + mb.removeTemplate(indexTemplateToDelete); + logger.debug("removing template [{}]", indexTemplateToDelete); + removedIndexTemplateName = indexTemplateToDelete; + } + + String attribute = nodeAttrName; + if (Strings.isNullOrEmpty(nodeAttrName)) { + attribute = DEFAULT_NODE_ATTRIBUTE_NAME; + } + List migratedPolicies = migrateIlmPolicies(mb, currentState, attribute, xContentRegistry, client); + List migratedIndices = migrateIndices(mb, currentState, attribute); + return Tuple.tuple(ClusterState.builder(currentState).metadata(mb).build(), + new MigratedEntities(removedIndexTemplateName, migratedIndices, migratedPolicies)); + } + + static List migrateIlmPolicies(Metadata.Builder mb, ClusterState currentState, String nodeAttrName, + NamedXContentRegistry xContentRegistry, Client client) { + List migratedPolicies = new ArrayList<>(); + IndexLifecycleMetadata currentLifecycleMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); + if (currentLifecycleMetadata != null) { + Map currentPolicies = currentLifecycleMetadata.getPolicyMetadatas(); + SortedMap newPolicies = new TreeMap<>(currentPolicies); + for (Map.Entry policyMetadataEntry : currentPolicies.entrySet()) { + LifecyclePolicy lifecyclePolicy = policyMetadataEntry.getValue().getPolicy(); + LifecyclePolicy newLifecylePolicy = null; + for (Map.Entry phaseEntry : lifecyclePolicy.getPhases().entrySet()) { + Phase phase = phaseEntry.getValue(); + AllocateAction allocateAction = (AllocateAction) phase.getActions().get(AllocateAction.NAME); + if (allocateActionDefinesRoutingRules(nodeAttrName, allocateAction)) { + Map actionMap = new HashMap<>(phase.getActions()); + // this phase contains an allocate action that defines a require rule for the attribute name so we'll remove all the + // rules to allow for the migrate action to be injected + if (allocateAction.getNumberOfReplicas() != null) { + // keep the number of replicas configuration + AllocateAction updatedAllocateAction = + new AllocateAction(allocateAction.getNumberOfReplicas(), null, null, null); + actionMap.put(allocateAction.getWriteableName(), updatedAllocateAction); + logger.debug("ILM policy [{}], phase [{}]: updated the allocate action to [{}]", lifecyclePolicy.getName(), + phase.getName(), allocateAction); + } else { + // remove the action altogether + actionMap.remove(allocateAction.getWriteableName()); + logger.debug("ILM policy [{}], phase [{}]: removed the allocate action", lifecyclePolicy.getName(), + phase.getName()); + } + + Phase updatedPhase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap); + Map updatedPhases = + new HashMap<>(newLifecylePolicy == null ? lifecyclePolicy.getPhases() : newLifecylePolicy.getPhases()); + updatedPhases.put(phaseEntry.getKey(), updatedPhase); + newLifecylePolicy = new LifecyclePolicy(lifecyclePolicy.getName(), updatedPhases); + } + } + + if (newLifecylePolicy != null) { + // we updated at least one phase + long nextVersion = policyMetadataEntry.getValue().getVersion() + 1L; + LifecyclePolicyMetadata newPolicyMetadata = new LifecyclePolicyMetadata(newLifecylePolicy, + policyMetadataEntry.getValue().getHeaders(), nextVersion, Instant.now().toEpochMilli()); + LifecyclePolicyMetadata oldPolicyMetadata = newPolicies.put(policyMetadataEntry.getKey(), newPolicyMetadata); + assert oldPolicyMetadata != null : + "we must only update policies, not create new ones, but " + policyMetadataEntry.getKey() + " didn't exist"; + + updateIndicesForPolicy(mb, currentState, xContentRegistry, client, oldPolicyMetadata.getPolicy(), newPolicyMetadata); + migratedPolicies.add(policyMetadataEntry.getKey()); + } + } + + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); + mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); + } + return migratedPolicies; + } + + static boolean allocateActionDefinesRoutingRules(String nodeAttrName, @Nullable AllocateAction allocateAction) { + return allocateAction != null && (allocateAction.getRequire().get(nodeAttrName) != null || + allocateAction.getInclude().get(nodeAttrName) != null || + allocateAction.getExclude().get(nodeAttrName) != null); + } + + static List migrateIndices(Metadata.Builder mb, ClusterState currentState, String nodeAttrName) { + List migratedIndices = new ArrayList<>(); + String nodeAttrIndexRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName; + for (ObjectObjectCursor index : currentState.metadata().indices()) { + IndexMetadata indexMetadata = index.value; + Settings currentIndexSettings = indexMetadata.getSettings(); + if (currentIndexSettings.keySet().contains(nodeAttrIndexRoutingSetting)) { + // look at the value, get the correct tiers config and update the settings and index metadata + Settings.Builder newSettingsBuilder = Settings.builder().put(currentIndexSettings); + String indexName = indexMetadata.getIndex().getName(); + if (currentIndexSettings.keySet().contains(INDEX_ROUTING_PREFER) == false) { + // parse the custom attribute routing into the corresponding tier preference and configure it + String attributeValue = currentIndexSettings.get(nodeAttrIndexRoutingSetting); + String convertedTierPreference = convertAttributeValueToTierPreference(attributeValue); + if (convertedTierPreference != null) { + newSettingsBuilder.put(INDEX_ROUTING_PREFER, convertedTierPreference); + newSettingsBuilder.remove(nodeAttrIndexRoutingSetting); + logger.debug("index [{}]: removed setting [{}]", indexName, nodeAttrIndexRoutingSetting); + logger.debug("index [{}]: configured setting [{}] to [{}]", indexName, + INDEX_ROUTING_PREFER, convertedTierPreference); + } else { + // log warning and do *not* remove setting + logger.warn("index [{}]: could not convert attribute based setting [{}] value of [{}] to a tier preference " + + "configuration. the only known values are: {}", indexName, + nodeAttrIndexRoutingSetting, attributeValue, "hot,warm,cold, and frozen"); + continue; + } + } else { + newSettingsBuilder.remove(nodeAttrIndexRoutingSetting); + logger.debug("index [{}]: removed setting [{}]", indexName, nodeAttrIndexRoutingSetting); + } + + Settings newSettings = newSettingsBuilder.build(); + if (currentIndexSettings.equals(newSettings) == false) { + mb.put(IndexMetadata.builder(indexMetadata) + .settings(newSettings) + .settingsVersion(indexMetadata.getSettingsVersion() + 1)); + migratedIndices.add(indexName); + } + } + } + return migratedIndices; + } + + /** + * Converts the provided node attribute value to the corresponding `_tier_preference` configuration. + * Known (and convertible) attribute values are: + * * hot + * * warm + * * cold + * * frozen + * and the corresponding tier preference setting values are, respectively: + * * data_hot + * * data_warm,data_hot + * * data_cold,data_warm,data_hot + * * data_frozen,data_cold,data_warm,data_hot + *

+ * This returns `null` if an unknown attribute value is received. + */ + @Nullable + static String convertAttributeValueToTierPreference(String nodeAttributeValue) { + String targetTier = "data_" + nodeAttributeValue; + // handle the `content` accidental node attribute value which would match a data tier but doesn't fall into the hot/warm/cold + // (given we're _migrating_ to data tiers we won't catch this accidental tier which didn't exist as a concept before the + // formalisation of data tiers) + if (DataTier.validTierName(targetTier) == false || targetTier.equals(DataTier.DATA_CONTENT)) { + return null; + } + return DataTier.getPreferredTiersConfiguration(targetTier); + } + + /** + * Represents the elasticsearch abstractions that were, in some way, migrated such that the system is managing indices lifecycles and + * allocations using data tiers. + */ + public static final class MigratedEntities { + @Nullable + public final String removedIndexTemplateName; + public final List migratedIndices; + public final List migratedPolicies; + + public MigratedEntities(@Nullable String removedIndexTemplateName, List migratedIndices, List migratedPolicies) { + this.removedIndexTemplateName = removedIndexTemplateName; + this.migratedIndices = Collections.unmodifiableList(migratedIndices); + this.migratedPolicies = Collections.unmodifiableList(migratedPolicies); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MigratedEntities that = (MigratedEntities) o; + return Objects.equals(removedIndexTemplateName, that.removedIndexTemplateName) && + Objects.equals(migratedIndices, that.migratedIndices) && + Objects.equals(migratedPolicies, that.migratedPolicies); + } + + @Override + public int hashCode() { + return Objects.hash(removedIndexTemplateName, migratedIndices, migratedPolicies); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/DataTier.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/DataTier.java index 3e9948c14bfba..4b1d5643316ad 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/DataTier.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/DataTier.java @@ -18,7 +18,9 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** * The {@code DataTier} class encapsulates the formalization of the "content", @@ -40,6 +42,10 @@ public class DataTier { public static final Set ALL_DATA_TIERS = new HashSet<>(Arrays.asList(DATA_CONTENT, DATA_HOT, DATA_WARM, DATA_COLD, DATA_FROZEN)); + // Represents an ordered list of data tiers from frozen to hot (or slow to fast) + private static final List ORDERED_FROZEN_TO_HOT_TIERS = + List.of(DataTier.DATA_FROZEN, DataTier.DATA_COLD, DataTier.DATA_WARM, DataTier.DATA_HOT); + /** * Returns true if the given tier name is a valid tier */ @@ -51,6 +57,19 @@ public static boolean validTierName(String tierName) { DATA_FROZEN.equals(tierName); } + /** + * Based on the provided target tier it will return a comma separated list of preferred tiers. + * ie. if `data_cold` is the target tier, it will return `data_cold,data_warm,data_hot` + * This is usually used in conjunction with {@link DataTierAllocationDecider#INDEX_ROUTING_PREFER_SETTING} + */ + public static String getPreferredTiersConfiguration(String targetTier) { + int indexOfTargetTier = ORDERED_FROZEN_TO_HOT_TIERS.indexOf(targetTier); + if (indexOfTargetTier == -1) { + throw new IllegalArgumentException("invalid data tier [" + targetTier + "]"); + } + return ORDERED_FROZEN_TO_HOT_TIERS.stream().skip(indexOfTargetTier).collect(Collectors.joining(",")); + } + /** * Returns true iff the given settings have a data tier setting configured */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MigrateAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MigrateAction.java index 7325c1c356090..b9edb7330dbbf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MigrateAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MigrateAction.java @@ -25,7 +25,8 @@ import java.io.IOException; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.DataTier.getPreferredTiersConfiguration; /** * A {@link LifecycleAction} which enables or disables the automatic migration of data between @@ -37,9 +38,6 @@ public class MigrateAction implements LifecycleAction { private static final Logger logger = LogManager.getLogger(MigrateAction.class); static final String CONDITIONAL_SKIP_MIGRATE_STEP = BranchingStep.NAME + "-check-skip-action"; - // Represents an ordered list of data tiers from frozen to hot (or slow to fast) - private static final List FROZEN_TO_HOT_TIERS = - List.of(DataTier.DATA_FROZEN, DataTier.DATA_COLD, DataTier.DATA_WARM, DataTier.DATA_HOT); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, a -> new MigrateAction(a[0] == null ? true : (boolean) a[0])); @@ -128,19 +126,6 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { } } - /** - * Based on the provided target tier it will return a comma separated list of preferred tiers. - * ie. if `data_cold` is the target tier, it will return `data_cold,data_warm,data_hot` - * This is usually used in conjunction with {@link DataTierAllocationDecider#INDEX_ROUTING_PREFER_SETTING} - */ - static String getPreferredTiersConfiguration(String targetTier) { - int indexOfTargetTier = FROZEN_TO_HOT_TIERS.indexOf(targetTier); - if (indexOfTargetTier == -1) { - throw new IllegalArgumentException("invalid data tier [" + targetTier + "]"); - } - return FROZEN_TO_HOT_TIERS.stream().skip(indexOfTargetTier).collect(Collectors.joining(",")); - } - @Override public int hashCode() { return Objects.hash(enabled); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java new file mode 100644 index 0000000000000..05ff3b1552f51 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java @@ -0,0 +1,242 @@ +/* + * 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.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; + +/** + * We cache the currently executing ILM phase in the index metadata so the ILM execution for managed indices is not irrecoverably + * interrupted by a concurrent update policy that, say, would remove the current execution phase altogether. + *

+ * This contains class contains a series of methods that help manage the cached ILM phase. + */ +public final class PhaseCacheManagement { + + private static final Logger logger = LogManager.getLogger(PhaseCacheManagement.class); + + private PhaseCacheManagement() { + } + + /** + * Rereads the phase JSON for the given index, returning a new cluster state. + */ + public static ClusterState refreshPhaseDefinition(final ClusterState state, final String index, + final LifecyclePolicyMetadata updatedPolicy) { + final IndexMetadata idxMeta = state.metadata().index(index); + Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()); + refreshPhaseDefinition(metadataBuilder, idxMeta, updatedPolicy); + return ClusterState.builder(state).metadata(metadataBuilder).build(); + } + + /** + * Rereads the phase JSON for the given index, and updates the provided metadata. + */ + public static void refreshPhaseDefinition(final Metadata.Builder metadataBuilder, final IndexMetadata idxMeta, + final LifecyclePolicyMetadata updatedPolicy) { + String index = idxMeta.getIndex().getName(); + assert eligibleToCheckForRefresh(idxMeta) : "index " + index + " is missing crucial information needed to refresh phase definition"; + + logger.trace("[{}] updating cached phase definition for policy [{}]", index, updatedPolicy.getName()); + LifecycleExecutionState currentExState = LifecycleExecutionState.fromIndexMetadata(idxMeta); + + String currentPhase = currentExState.getPhase(); + PhaseExecutionInfo pei = new PhaseExecutionInfo(updatedPolicy.getName(), + updatedPolicy.getPolicy().getPhases().get(currentPhase), updatedPolicy.getVersion(), updatedPolicy.getModifiedDate()); + + LifecycleExecutionState newExState = LifecycleExecutionState.builder(currentExState) + .setPhaseDefinition(Strings.toString(pei, false, false)) + .build(); + + metadataBuilder.put(IndexMetadata.builder(idxMeta) + .putCustom(ILM_CUSTOM_METADATA_KEY, newExState.asMap())); + } + + + /** + * Ensure that we have the minimum amount of metadata necessary to check for cache phase + * refresh. This includes: + * - An execution state + * - Existing phase definition JSON + * - A current step key + * - A current phase in the step key + * - Not currently in the ERROR step + */ + public static boolean eligibleToCheckForRefresh(final IndexMetadata metadata) { + LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(metadata); + if (executionState == null || executionState.getPhaseDefinition() == null) { + return false; + } + + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(executionState); + if (currentStepKey == null || currentStepKey.getPhase() == null) { + return false; + } + + return ErrorStep.NAME.equals(currentStepKey.getName()) == false; + } + + /** + * For the given new policy, returns a new cluster with all updateable indices' phase JSON refreshed. + */ + public static ClusterState updateIndicesForPolicy(final ClusterState state, final NamedXContentRegistry xContentRegistry, + final Client client, final LifecyclePolicy oldPolicy, + final LifecyclePolicyMetadata newPolicy) { + Metadata.Builder mb = Metadata.builder(state.metadata()); + if (updateIndicesForPolicy(mb, state, xContentRegistry, client, oldPolicy, newPolicy)) { + return ClusterState.builder(state).metadata(mb).build(); + } + return state; + } + + /** + * For the given new policy, update the provided metadata to reflect the refreshed phase JSON for all updateable indices. + * Returns true if any indices were updated and false otherwise. + * Users of this API should consider the returned value and only create a new {@link ClusterState} if `true` is returned. + */ + public static boolean updateIndicesForPolicy(final Metadata.Builder mb, final ClusterState currentState, + final NamedXContentRegistry xContentRegistry, final Client client, + final LifecyclePolicy oldPolicy, final LifecyclePolicyMetadata newPolicy) { + assert oldPolicy.getName().equals(newPolicy.getName()) : "expected both policies to have the same id but they were: [" + + oldPolicy.getName() + "] vs. [" + newPolicy.getName() + "]"; + + // No need to update anything if the policies are identical in contents + if (oldPolicy.equals(newPolicy.getPolicy())) { + logger.debug("policy [{}] is unchanged and no phase definition refresh is needed", oldPolicy.getName()); + return false; + } + + final List indicesThatCanBeUpdated = + StreamSupport.stream(Spliterators.spliteratorUnknownSize(currentState.metadata().indices().valuesIt(), 0), false) + .filter(meta -> newPolicy.getName().equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(meta.getSettings()))) + .filter(meta -> isIndexPhaseDefinitionUpdatable(xContentRegistry, client, meta, newPolicy.getPolicy())) + .collect(Collectors.toList()); + + for (IndexMetadata index : indicesThatCanBeUpdated) { + try { + refreshPhaseDefinition(mb, index, newPolicy); + } catch (Exception e) { + logger.warn(new ParameterizedMessage("[{}] unable to refresh phase definition for updated policy [{}]", + index, newPolicy.getName()), e); + } + } + + return indicesThatCanBeUpdated.size() > 0; + } + + /** + * Returns 'true' if the index's cached phase JSON can be safely reread, 'false' otherwise. + */ + public static boolean isIndexPhaseDefinitionUpdatable(final NamedXContentRegistry xContentRegistry, final Client client, + final IndexMetadata metadata, final LifecyclePolicy newPolicy) { + final String index = metadata.getIndex().getName(); + if (eligibleToCheckForRefresh(metadata) == false) { + logger.debug("[{}] does not contain enough information to check for eligibility of refreshing phase", index); + return false; + } + final String policyId = newPolicy.getName(); + + final LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(metadata); + final Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(executionState); + final String currentPhase = currentStepKey.getPhase(); + + final Set newStepKeys = newPolicy.toSteps(client).stream() + .map(Step::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (newStepKeys.contains(currentStepKey) == false) { + // The index is on a step that doesn't exist in the new policy, we + // can't safely re-read the JSON + logger.debug("[{}] updated policy [{}] does not contain the current step key [{}], so the policy phase will not be refreshed", + index, policyId, currentStepKey); + return false; + } + + final String phaseDef = executionState.getPhaseDefinition(); + final Set oldStepKeys = readStepKeys(xContentRegistry, client, phaseDef, currentPhase); + if (oldStepKeys == null) { + logger.debug("[{}] unable to parse phase definition for cached policy [{}], policy phase will not be refreshed", + index, policyId); + return false; + } + + final Set oldPhaseStepKeys = oldStepKeys.stream() + .filter(sk -> currentPhase.equals(sk.getPhase())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + final PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(policyId, newPolicy.getPhases().get(currentPhase), 1L, 1L); + final String peiJson = Strings.toString(phaseExecutionInfo); + + final Set newPhaseStepKeys = readStepKeys(xContentRegistry, client, peiJson, currentPhase); + if (newPhaseStepKeys == null) { + logger.debug(new ParameterizedMessage("[{}] unable to parse phase definition for policy [{}] " + + "to determine if it could be refreshed", index, policyId)); + return false; + } + + if (newPhaseStepKeys.equals(oldPhaseStepKeys)) { + // The new and old phase have the same stepkeys for this current phase, so we can + // refresh the definition because we know it won't change the execution flow. + logger.debug("[{}] updated policy [{}] contains the same phase step keys and can be refreshed", index, policyId); + return true; + } else { + logger.debug("[{}] updated policy [{}] has different phase step keys and will NOT refresh phase " + + "definition as it differs too greatly. old: {}, new: {}", + index, policyId, oldPhaseStepKeys, newPhaseStepKeys); + return false; + } + } + + /** + * Parse the {@code phaseDef} phase definition to get the stepkeys for the given phase. + * If there is an error parsing or if the phase definition is missing the required + * information, returns null. + */ + @Nullable + static Set readStepKeys(final NamedXContentRegistry xContentRegistry, final Client client, + final String phaseDef, final String currentPhase) { + final PhaseExecutionInfo phaseExecutionInfo; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(xContentRegistry, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, phaseDef)) { + phaseExecutionInfo = PhaseExecutionInfo.parse(parser, currentPhase); + } catch (Exception e) { + logger.trace(new ParameterizedMessage("exception reading step keys checking for refreshability, phase definition: {}", + phaseDef), e); + return null; + } + + if (phaseExecutionInfo == null || phaseExecutionInfo.getPhase() == null) { + return null; + } + + return phaseExecutionInfo.getPhase().getActions().values().stream() + .flatMap(a -> a.toSteps(client, phaseExecutionInfo.getPhase().getName(), null).stream()) + .map(Step::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java new file mode 100644 index 0000000000000..57ec10d4f18fc --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -0,0 +1,410 @@ +/* + * 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.cluster.metadata; + +import org.elasticsearch.Version; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexTemplateMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.MigratedEntities; +import org.elasticsearch.xpack.core.ilm.AllocateAction; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleSettings; +import org.elasticsearch.xpack.core.ilm.OperationMode; +import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.ShrinkAction; +import org.junit.Before; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; +import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.allocateActionDefinesRoutingRules; +import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.convertAttributeValueToTierPreference; +import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateIlmPolicies; +import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateIndices; +import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateToDataTiersRouting; +import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; + +public class MetadataMigrateToDataTiersRoutingServiceTests extends ESTestCase { + + private static final String DATA_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "data"; + private static final String BOX_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "box"; + private static final NamedXContentRegistry REGISTRY; + + static { + REGISTRY = new NamedXContentRegistry(List.of( + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse) + )); + } + + private String lifecycleName; + private String indexName; + private Client client; + + @Before + public void setupTestEntities() { + lifecycleName = randomAlphaOfLengthBetween(10, 15); + indexName = randomAlphaOfLengthBetween(10, 15); + client = mock(Client.class); + logger.info("--> running [{}] with indexName [{}] and ILM policy [{}]", getTestName(), indexName, lifecycleName); + } + + public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { + ShrinkAction shrinkAction = new ShrinkAction(2, null); + AllocateAction warmAllocateAction = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); + AllocateAction coldAllocateAction = new AllocateAction(0, null, null, Map.of("data", "cold")); + LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(shrinkAction, warmAllocateAction, coldAllocateAction); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.RUNNING)) + .put(IndexMetadata.builder(indexName).settings(getBaseIndexSettings())).build()) + .build(); + + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + assertThat(migratedPolicies.size(), is(1)); + assertThat(migratedPolicies.get(0), is(lifecycleName)); + + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + IndexLifecycleMetadata updatedLifecycleMetadata = newState.metadata().custom(IndexLifecycleMetadata.TYPE); + LifecyclePolicy lifecyclePolicy = updatedLifecycleMetadata.getPolicies().get(lifecycleName); + Map warmActions = lifecyclePolicy.getPhases().get("warm").getActions(); + assertThat("allocate action in the warm phase didn't specify any number of replicas so it must be removed", + warmActions.size(), is(1)); + assertThat(warmActions.get(shrinkAction.getWriteableName()), is(shrinkAction)); + + Map coldActions = lifecyclePolicy.getPhases().get("cold").getActions(); + assertThat(coldActions.size(), is(1)); + AllocateAction migratedColdAllocateAction = (AllocateAction) coldActions.get(coldAllocateAction.getWriteableName()); + assertThat(migratedColdAllocateAction.getNumberOfReplicas(), is(0)); + assertThat(migratedColdAllocateAction.getRequire().size(), is(0)); + } + + public void testMigrateIlmPolicyRefreshesCachedPhase() { + ShrinkAction shrinkAction = new ShrinkAction(2, null); + AllocateAction warmAllocateAction = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); + AllocateAction coldAllocateAction = new AllocateAction(0, null, null, Map.of("data", "cold")); + LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(shrinkAction, warmAllocateAction, coldAllocateAction); + + LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() + .setPhase("cold") + .setAction("allocate") + .setStep("allocate") + .setPhaseDefinition("{\n" + + " \"policy\" : \"" + lifecycleName + "\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"0m\",\n" + + " \"actions\" : {\n" + + " \"allocate\" : {\n" + + " \"number_of_replicas\" : \"0\",\n" + + " \"require\" : {\n" + + " \"data\": \"cold\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }") + .build(); + + IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) + .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.RUNNING)) + .put(indexMetadata).build()) + .build(); + + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + + assertThat(migratedPolicies.get(0), is(lifecycleName)); + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + + // expecting the phase definition to be refreshed with the migrated phase representation + // ie. allocate action does not contain any allocation rules + String expectedRefreshedPhaseDefinition = "\"phase_definition\":{\"min_age\":\"0ms\"," + + "\"actions\":{\"allocate\":{\"number_of_replicas\":0,\"include\":{},\"exclude\":{},\"require\":{}}}}"; + assertThat(newLifecycleState.getPhaseDefinition(), containsString(expectedRefreshedPhaseDefinition)); + } + + private Settings.Builder getBaseIndexSettings() { + Settings.Builder settings = Settings.builder() + .put(LifecycleSettings.LIFECYCLE_NAME, lifecycleName) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT); + return settings; + } + + public void testAllocateActionDefinesRoutingRules() { + assertThat(allocateActionDefinesRoutingRules("data", new AllocateAction(null, Map.of("data", "cold"), null, null)), is(true)); + assertThat(allocateActionDefinesRoutingRules("data", new AllocateAction(null, null, Map.of("data", "cold"), null)), is(true)); + assertThat(allocateActionDefinesRoutingRules("data", new AllocateAction(null, Map.of("another_attribute", "rack1"), null, + Map.of("data", "cold"))), is(true)); + assertThat(allocateActionDefinesRoutingRules("data", new AllocateAction(null, null, null, Map.of("another_attribute", "cold"))), + is(false)); + assertThat(allocateActionDefinesRoutingRules("data", null), is(false)); + } + + public void testConvertAttributeValueToTierPreference() { + assertThat(convertAttributeValueToTierPreference("frozen"), is("data_frozen,data_cold,data_warm,data_hot")); + assertThat(convertAttributeValueToTierPreference("cold"), is("data_cold,data_warm,data_hot")); + assertThat(convertAttributeValueToTierPreference("warm"), is("data_warm,data_hot")); + assertThat(convertAttributeValueToTierPreference("hot"), is("data_hot")); + assertThat(convertAttributeValueToTierPreference("content"), nullValue()); + assertThat(convertAttributeValueToTierPreference("rack1"), nullValue()); + } + + public void testMigrateIndices() { + { + // index with `warm` data attribute is migrated to the equivalent _tier_preference routing + IndexMetadata.Builder indexWitWarmDataAttribute = + IndexMetadata.builder("indexWitWarmDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, + "warm")); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWitWarmDataAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWitWarmDataAttribute")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWitWarmDataAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + + { + // since the index has a _tier_preference configuration the migrated index should still contain it and have the `data` + // attribute routing removed + IndexMetadata.Builder indexWithTierPreferenceAndDataAttribute = + IndexMetadata.builder("indexWithTierPreferenceAndDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, + "cold").put(INDEX_ROUTING_PREFER, "data_warm,data_hot")); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithTierPreferenceAndDataAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWithTierPreferenceAndDataAttribute")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithTierPreferenceAndDataAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + + { + // index with an unknown `data` attribute routing value should **not** be migrated + IndexMetadata.Builder indexWithUnknownDataAttribute = + IndexMetadata.builder("indexWithUnknownDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, + "something_else")); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithUnknownDataAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(0)); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithUnknownDataAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), is("something_else")); + } + + { + // index with data and another attribute should only see the data attribute removed and the corresponding tier_preference + // configured + IndexMetadata.Builder indexDataAndBoxAttribute = + IndexMetadata.builder("indexWithDataAndBoxAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, + "warm").put(BOX_ROUTING_REQUIRE_SETTING, "box1")); + + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexDataAndBoxAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWithDataAndBoxAttribute")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithDataAndBoxAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(BOX_ROUTING_REQUIRE_SETTING), is("box1")); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + + { + // index that doesn't have any data attribute routing but has another attribute should not see any change + IndexMetadata.Builder indexBoxAttribute = + IndexMetadata.builder("indexWithBoxAttribute").settings(getBaseIndexSettings().put(BOX_ROUTING_REQUIRE_SETTING, "warm")); + + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexBoxAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(0)); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithBoxAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(BOX_ROUTING_REQUIRE_SETTING), is("warm")); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), nullValue()); + } + + { + IndexMetadata.Builder indexNoRoutingAttribute = + IndexMetadata.builder("indexNoRoutingAttribute").settings(getBaseIndexSettings()); + + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexNoRoutingAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(0)); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexNoRoutingAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(BOX_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), nullValue()); + } + } + + public void testMigrateToDataTiersRouting() { + AllocateAction allocateActionWithDataAttribute = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); + AllocateAction allocateActionWithOtherAttribute = new AllocateAction(0, null, null, Map.of("other", "cold")); + + LifecyclePolicy policyToMigrate = new LifecyclePolicy(lifecycleName, + Map.of("warm", + new Phase("warm", TimeValue.ZERO, Map.of(allocateActionWithDataAttribute.getWriteableName(), + allocateActionWithDataAttribute)))); + LifecyclePolicyMetadata policyWithDataAttribute = new LifecyclePolicyMetadata(policyToMigrate, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + + LifecyclePolicy shouldntBeMigratedPolicy = new LifecyclePolicy("dont-migrate", + Map.of("warm", + new Phase("warm", TimeValue.ZERO, Map.of(allocateActionWithOtherAttribute.getWriteableName(), + allocateActionWithOtherAttribute)))); + LifecyclePolicyMetadata policyWithOtherAttribute = new LifecyclePolicyMetadata(shouldntBeMigratedPolicy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + + + IndexMetadata.Builder indexWithUnknownDataAttribute = + IndexMetadata.builder("indexWithUnknownDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, + "something_else")); + IndexMetadata.Builder indexWitWarmDataAttribute = + IndexMetadata.builder("indexWitWarmDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, "warm")); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Map.of(policyToMigrate.getName(), policyWithDataAttribute, shouldntBeMigratedPolicy.getName(), policyWithOtherAttribute), + OperationMode.RUNNING)) + .put(IndexTemplateMetadata.builder("catch-all").patterns(List.of("*")) + .settings(Settings.builder().put(DATA_ROUTING_REQUIRE_SETTING, "hot")) + .build()) + .put(IndexTemplateMetadata.builder("other-template").patterns(List.of("other-*")) + .settings(Settings.builder().put(DATA_ROUTING_REQUIRE_SETTING, "hot")) + .build()) + .put(indexWithUnknownDataAttribute).put(indexWitWarmDataAttribute)) + .build(); + + { + Tuple migratedEntitiesTuple = + migrateToDataTiersRouting(state, "data", "catch-all", REGISTRY, client); + + MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); + assertThat(migratedEntities.removedIndexTemplateName, is("catch-all")); + assertThat(migratedEntities.migratedPolicies.size(), is(1)); + assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices.size(), is(1)); + assertThat(migratedEntities.migratedIndices.get(0), is("indexWitWarmDataAttribute")); + + ClusterState newState = migratedEntitiesTuple.v1(); + assertThat(newState.metadata().getTemplates().size(), is(1)); + assertThat(newState.metadata().getTemplates().get("catch-all"), nullValue()); + assertThat(newState.metadata().getTemplates().get("other-template"), notNullValue()); + } + + { + // let's test a null template name to make sure nothing is removed + Tuple migratedEntitiesTuple = + migrateToDataTiersRouting(state, "data", null, REGISTRY, client); + + MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); + assertThat(migratedEntities.removedIndexTemplateName, nullValue()); + assertThat(migratedEntities.migratedPolicies.size(), is(1)); + assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices.size(), is(1)); + assertThat(migratedEntities.migratedIndices.get(0), is("indexWitWarmDataAttribute")); + + ClusterState newState = migratedEntitiesTuple.v1(); + assertThat(newState.metadata().getTemplates().size(), is(2)); + assertThat(newState.metadata().getTemplates().get("catch-all"), notNullValue()); + assertThat(newState.metadata().getTemplates().get("other-template"), notNullValue()); + } + + { + // let's test a null node attribute parameter defaults to "data" + Tuple migratedEntitiesTuple = + migrateToDataTiersRouting(state, null, null, REGISTRY, client); + + MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); + assertThat(migratedEntities.migratedPolicies.size(), is(1)); + assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices.size(), is(1)); + assertThat(migratedEntities.migratedIndices.get(0), is("indexWitWarmDataAttribute")); + + IndexMetadata migratedIndex = migratedEntitiesTuple.v1().metadata().index("indexWitWarmDataAttribute"); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + } + + private LifecyclePolicyMetadata getWarmColdPolicyMeta(ShrinkAction shrinkAction, AllocateAction warmAllocateAction, + AllocateAction coldAllocateAction) { + LifecyclePolicy policy = new LifecyclePolicy(lifecycleName, + Map.of("warm", + new Phase("warm", TimeValue.ZERO, Map.of(shrinkAction.getWriteableName(), shrinkAction, + warmAllocateAction.getWriteableName(), warmAllocateAction)), + "cold", + new Phase("cold", TimeValue.ZERO, Map.of(coldAllocateAction.getWriteableName(), coldAllocateAction)) + )); + return new LifecyclePolicyMetadata(policy, Collections.emptyMap(), + randomNonNegativeLong(), randomNonNegativeLong()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/DataTierTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/DataTierTests.java index cb46f0473c39e..bdf611eac53a8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/DataTierTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/DataTierTests.java @@ -26,6 +26,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.StreamSupport; +import static org.elasticsearch.xpack.core.DataTier.DATA_COLD; +import static org.elasticsearch.xpack.core.DataTier.DATA_HOT; +import static org.elasticsearch.xpack.core.DataTier.DATA_WARM; +import static org.elasticsearch.xpack.core.DataTier.getPreferredTiersConfiguration; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; @@ -110,6 +115,14 @@ public void testDataRoleDoesNotImplyTieredDataRoles() { assertThat(node.getRoles(), not(hasItem(DiscoveryNodeRole.DATA_COLD_NODE_ROLE))); } + public void testGetPreferredTiersConfiguration() { + assertThat(getPreferredTiersConfiguration(DATA_HOT), is(DATA_HOT)); + assertThat(getPreferredTiersConfiguration(DATA_WARM), is(DATA_WARM + "," + DATA_HOT)); + assertThat(getPreferredTiersConfiguration(DATA_COLD), is(DATA_COLD + "," + DATA_WARM + "," + DATA_HOT)); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> getPreferredTiersConfiguration("no_tier")); + assertThat(exception.getMessage(), is("invalid data tier [no_tier]")); + } + private static DiscoveryNodes buildDiscoveryNodes() { int numNodes = randomIntBetween(3, 15); DiscoveryNodes.Builder discoBuilder = DiscoveryNodes.builder(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MigrateActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MigrateActionTests.java index dd6c35a64c9dc..98fad37d6923c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MigrateActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MigrateActionTests.java @@ -23,7 +23,6 @@ import static org.elasticsearch.xpack.core.DataTier.DATA_COLD; import static org.elasticsearch.xpack.core.DataTier.DATA_HOT; import static org.elasticsearch.xpack.core.DataTier.DATA_WARM; -import static org.elasticsearch.xpack.core.ilm.MigrateAction.getPreferredTiersConfiguration; import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.COLD_PHASE; import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.DELETE_PHASE; import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.HOT_PHASE; @@ -83,14 +82,6 @@ public void testToSteps() { } } - public void testGetPreferredTiersConfiguration() { - assertThat(getPreferredTiersConfiguration(DATA_HOT), is(DATA_HOT)); - assertThat(getPreferredTiersConfiguration(DATA_WARM), is(DATA_WARM + "," + DATA_HOT)); - assertThat(getPreferredTiersConfiguration(DATA_COLD), is(DATA_COLD + "," + DATA_WARM + "," + DATA_HOT)); - IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> getPreferredTiersConfiguration("no_tier")); - assertThat(exception.getMessage(), is("invalid data tier [no_tier]")); - } - public void testMigrateActionsConfiguresTierPreference() { StepKey nextStepKey = new StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10)); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java similarity index 73% rename from x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java index c707b9ebe6c23..86339b2297b5d 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java @@ -5,86 +5,154 @@ * 2.0. */ -package org.elasticsearch.xpack.ilm.action; +package org.elasticsearch.xpack.core.ilm; import org.elasticsearch.Version; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.ilm.AllocateAction; -import org.elasticsearch.xpack.core.ilm.AllocationRoutedStep; -import org.elasticsearch.xpack.core.ilm.CheckNotDataStreamWriteIndexStep; -import org.elasticsearch.xpack.core.ilm.ErrorStep; -import org.elasticsearch.xpack.core.ilm.ForceMergeAction; -import org.elasticsearch.xpack.core.ilm.FreezeAction; -import org.elasticsearch.xpack.core.ilm.LifecycleAction; -import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; -import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; -import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; -import org.elasticsearch.xpack.core.ilm.LifecycleSettings; -import org.elasticsearch.xpack.core.ilm.Phase; -import org.elasticsearch.xpack.core.ilm.PhaseExecutionInfo; -import org.elasticsearch.xpack.core.ilm.ReadOnlyAction; -import org.elasticsearch.xpack.core.ilm.RolloverAction; -import org.elasticsearch.xpack.core.ilm.RolloverStep; -import org.elasticsearch.xpack.core.ilm.SegmentCountStep; -import org.elasticsearch.xpack.core.ilm.SetPriorityAction; -import org.elasticsearch.xpack.core.ilm.Step; -import org.elasticsearch.xpack.core.ilm.UpdateRolloverLifecycleDateStep; -import org.elasticsearch.xpack.core.ilm.WaitForActiveShardsStep; -import org.elasticsearch.xpack.core.ilm.WaitForRolloverReadyStep; -import org.elasticsearch.xpack.ilm.IndexLifecycle; - -import java.util.ArrayList; + import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.eligibleToCheckForRefresh; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.isIndexPhaseDefinitionUpdatable; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.readStepKeys; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.refreshPhaseDefinition; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; -public class TransportPutLifecycleActionTests extends ESTestCase { +public class PhaseCacheManagementTests extends ESTestCase { + private static final NamedXContentRegistry REGISTRY; private static final Client client = mock(Client.class); private static final String index = "eggplant"; static { - try (IndexLifecycle indexLifecycle = new IndexLifecycle(Settings.EMPTY)) { - List entries = new ArrayList<>(indexLifecycle.getNamedXContent()); - REGISTRY = new NamedXContentRegistry(entries); - } + REGISTRY = new NamedXContentRegistry(List.of( + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse)) + ); + } + + public void testRefreshPhaseJson() { + LifecycleExecutionState.Builder exState = LifecycleExecutionState.builder() + .setPhase("hot") + .setAction("rollover") + .setStep("check-rollover-ready") + .setPhaseDefinition("{\n" + + " \"policy\" : \"my-policy\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"20m\",\n" + + " \"actions\" : {\n" + + " \"rollover\" : {\n" + + " \"max_age\" : \"5s\"\n" + + " },\n" + + " \"set_priority\" : {\n" + + " \"priority\" : 150\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }"); + + IndexMetadata meta = buildIndexMetadata("my-policy", exState); + String index = meta.getIndex().getName(); + + Map actions = new HashMap<>(); + actions.put("rollover", new RolloverAction(null, null, null, 1L)); + actions.put("set_priority", new SetPriorityAction(100)); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); + Map phases = Collections.singletonMap("hot", hotPhase); + LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + + ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(Metadata.builder(Metadata.EMPTY_METADATA) + .put(meta, false) + .build()) + .build(); + + ClusterState changedState = refreshPhaseDefinition(existingState, index, policyMetadata); + + IndexMetadata newIdxMeta = changedState.metadata().index(index); + LifecycleExecutionState afterExState = LifecycleExecutionState.fromIndexMetadata(newIdxMeta); + Map beforeState = new HashMap<>(exState.build().asMap()); + beforeState.remove("phase_definition"); + Map afterState = new HashMap<>(afterExState.asMap()); + afterState.remove("phase_definition"); + // Check that no other execution state changes have been made + assertThat(beforeState, equalTo(afterState)); + + // Check that the phase definition has been refreshed + assertThat(afterExState.getPhaseDefinition(), + equalTo("{\"policy\":\"my-policy\",\"phase_definition\":{\"min_age\":\"0ms\",\"actions\":{\"rollover\":{\"max_docs\":1}," + + "\"set_priority\":{\"priority\":100}}},\"version\":2,\"modified_date_in_millis\":2}")); } public void testEligibleForRefresh() { - IndexMetadata meta = mkMeta().build(); - assertFalse(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + IndexMetadata meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); LifecycleExecutionState state = LifecycleExecutionState.builder().build(); - meta = mkMeta().putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()).build(); - assertFalse(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); state = LifecycleExecutionState.builder() .setPhase("phase") .setAction("action") .setStep("step") .build(); - meta = mkMeta().putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()).build(); - assertFalse(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); state = LifecycleExecutionState.builder() .setPhaseDefinition("{}") .build(); - meta = mkMeta().putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()).build(); - assertFalse(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); state = LifecycleExecutionState.builder() .setPhase("phase") @@ -92,8 +160,15 @@ public void testEligibleForRefresh() { .setStep(ErrorStep.NAME) .setPhaseDefinition("{}") .build(); - meta = mkMeta().putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()).build(); - assertFalse(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); state = LifecycleExecutionState.builder() .setPhase("phase") @@ -101,35 +176,42 @@ public void testEligibleForRefresh() { .setStep("step") .setPhaseDefinition("{}") .build(); - meta = mkMeta().putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()).build(); - assertTrue(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertTrue(eligibleToCheckForRefresh(meta)); } public void testReadStepKeys() { - assertNull(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, "{}", "phase")); - assertNull(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, "aoeu", "phase")); - assertNull(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, "", "phase")); - - assertThat(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, "{\n" + - " \"policy\": \"my_lifecycle3\",\n" + - " \"phase_definition\": { \n" + - " \"min_age\": \"0ms\",\n" + - " \"actions\": {\n" + - " \"rollover\": {\n" + - " \"max_age\": \"30s\"\n" + - " }\n" + - " }\n" + - " },\n" + - " \"version\": 3, \n" + - " \"modified_date_in_millis\": 1539609701576 \n" + - " }", "phase"), + assertNull(readStepKeys(REGISTRY, client, "{}", "phase")); + assertNull(readStepKeys(REGISTRY, client, "aoeu", "phase")); + assertNull(readStepKeys(REGISTRY, client, "", "phase")); + + assertThat(readStepKeys(REGISTRY, client, "{\n" + + " \"policy\": \"my_lifecycle3\",\n" + + " \"phase_definition\": { \n" + + " \"min_age\": \"0ms\",\n" + + " \"actions\": {\n" + + " \"rollover\": {\n" + + " \"max_age\": \"30s\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\": 3, \n" + + " \"modified_date_in_millis\": 1539609701576 \n" + + " }", "phase"), contains(new Step.StepKey("phase", "rollover", WaitForRolloverReadyStep.NAME), new Step.StepKey("phase", "rollover", RolloverStep.NAME), new Step.StepKey("phase", "rollover", WaitForActiveShardsStep.NAME), new Step.StepKey("phase", "rollover", UpdateRolloverLifecycleDateStep.NAME), new Step.StepKey("phase", "rollover", RolloverAction.INDEXING_COMPLETE_STEP_NAME))); - assertThat(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, "{\n" + + assertThat(readStepKeys(REGISTRY, client, "{\n" + " \"policy\" : \"my_lifecycle3\",\n" + " \"phase_definition\" : {\n" + " \"min_age\" : \"20m\",\n" + @@ -154,17 +236,13 @@ public void testReadStepKeys() { Map actions = new HashMap<>(); actions.put("forcemerge", new ForceMergeAction(5, null)); - actions.put("freeze", new FreezeAction()); actions.put("allocate", new AllocateAction(1, null, null, null)); PhaseExecutionInfo pei = new PhaseExecutionInfo("policy", new Phase("wonky", TimeValue.ZERO, actions), 1, 1); String phaseDef = Strings.toString(pei); logger.info("--> phaseDef: {}", phaseDef); - assertThat(TransportPutLifecycleAction.readStepKeys(REGISTRY, client, phaseDef, "phase"), + assertThat(readStepKeys(REGISTRY, client, phaseDef, "phase"), contains( - new Step.StepKey("phase", "freeze", FreezeAction.CONDITIONAL_SKIP_FREEZE_STEP), - new Step.StepKey("phase", "freeze", CheckNotDataStreamWriteIndexStep.NAME), - new Step.StepKey("phase", "freeze", FreezeAction.NAME), new Step.StepKey("phase", "allocate", AllocateAction.NAME), new Step.StepKey("phase", "allocate", AllocationRoutedStep.NAME), new Step.StepKey("phase", "forcemerge", ForceMergeAction.CONDITIONAL_SKIP_FORCE_MERGE_STEP), @@ -212,7 +290,7 @@ public void testIndexCanBeSafelyUpdated() { Map phases = Collections.singletonMap("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - assertTrue(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertTrue(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); } // Failure case, can't update because the step we're currently on has been removed in the new policy @@ -249,7 +327,7 @@ public void testIndexCanBeSafelyUpdated() { Map phases = Collections.singletonMap("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - assertFalse(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); } // Failure case, can't update because the future step has been deleted @@ -286,7 +364,7 @@ public void testIndexCanBeSafelyUpdated() { Map phases = Collections.singletonMap("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - assertFalse(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); } // Failure case, index doesn't have enough info to check @@ -321,7 +399,7 @@ public void testIndexCanBeSafelyUpdated() { Map phases = Collections.singletonMap("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - assertFalse(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); } // Failure case, the phase JSON is unparseable @@ -344,68 +422,10 @@ public void testIndexCanBeSafelyUpdated() { Map phases = Collections.singletonMap("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - assertFalse(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); } } - public void testRefreshPhaseJson() { - LifecycleExecutionState exState = LifecycleExecutionState.builder() - .setPhase("hot") - .setAction("rollover") - .setStep("check-rollover-ready") - .setPhaseDefinition("{\n" + - " \"policy\" : \"my-policy\",\n" + - " \"phase_definition\" : {\n" + - " \"min_age\" : \"20m\",\n" + - " \"actions\" : {\n" + - " \"rollover\" : {\n" + - " \"max_age\" : \"5s\"\n" + - " },\n" + - " \"set_priority\" : {\n" + - " \"priority\" : 150\n" + - " }\n" + - " }\n" + - " },\n" + - " \"version\" : 1,\n" + - " \"modified_date_in_millis\" : 1578521007076\n" + - " }") - .build(); - - IndexMetadata meta = mkMeta() - .putCustom(ILM_CUSTOM_METADATA_KEY, exState.asMap()) - .build(); - - Map actions = new HashMap<>(); - actions.put("rollover", new RolloverAction(null, null, null, 1L)); - actions.put("set_priority", new SetPriorityAction(100)); - Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); - LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); - - ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) - .metadata(Metadata.builder(Metadata.EMPTY_METADATA) - .put(meta, false) - .build()) - .build(); - - ClusterState changedState = TransportPutLifecycleAction.refreshPhaseDefinition(existingState, index, policyMetadata); - - IndexMetadata newIdxMeta = changedState.metadata().index(index); - LifecycleExecutionState afterExState = LifecycleExecutionState.fromIndexMetadata(newIdxMeta); - Map beforeState = new HashMap<>(exState.asMap()); - beforeState.remove("phase_definition"); - Map afterState = new HashMap<>(afterExState.asMap()); - afterState.remove("phase_definition"); - // Check that no other execution state changes have been made - assertThat(beforeState, equalTo(afterState)); - - // Check that the phase definition has been refreshed - assertThat(afterExState.getPhaseDefinition(), - equalTo("{\"policy\":\"my-policy\",\"phase_definition\":{\"min_age\":\"0ms\",\"actions\":{\"rollover\":{\"max_docs\":1}," + - "\"set_priority\":{\"priority\":100}}},\"version\":2,\"modified_date_in_millis\":2}")); - } - public void testUpdateIndicesForPolicy() { LifecycleExecutionState exState = LifecycleExecutionState.builder() .setPhase("hot") @@ -419,7 +439,7 @@ public void testUpdateIndicesForPolicy() { .putCustom(ILM_CUSTOM_METADATA_KEY, exState.asMap()) .build(); - assertTrue(TransportPutLifecycleAction.eligibleToCheckForRefresh(meta)); + assertTrue(eligibleToCheckForRefresh(meta)); Map oldActions = new HashMap<>(); oldActions.put("rollover", new RolloverAction(null, null, null, 1L)); @@ -436,7 +456,7 @@ public void testUpdateIndicesForPolicy() { LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); - assertTrue(TransportPutLifecycleAction.isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); + assertTrue(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy)); ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) .metadata(Metadata.builder(Metadata.EMPTY_METADATA) @@ -445,8 +465,7 @@ public void testUpdateIndicesForPolicy() { .build(); logger.info("--> update for unchanged policy"); - ClusterState updatedState = TransportPutLifecycleAction.updateIndicesForPolicy(existingState, REGISTRY, - client, oldPolicy, policyMetadata); + ClusterState updatedState = updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata); // No change, because the policies were identical assertThat(updatedState, equalTo(existingState)); @@ -460,7 +479,7 @@ public void testUpdateIndicesForPolicy() { policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); logger.info("--> update with changed policy, but not configured in settings"); - updatedState = TransportPutLifecycleAction.updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata); + updatedState = updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata); // No change, because the index doesn't have a lifecycle.name setting for this policy assertThat(updatedState, equalTo(existingState)); @@ -481,7 +500,7 @@ public void testUpdateIndicesForPolicy() { .build(); logger.info("--> update with changed policy and this index has the policy"); - updatedState = TransportPutLifecycleAction.updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata); + updatedState = updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata); IndexMetadata newIdxMeta = updatedState.metadata().index(index); LifecycleExecutionState afterExState = LifecycleExecutionState.fromIndexMetadata(newIdxMeta); @@ -498,6 +517,15 @@ public void testUpdateIndicesForPolicy() { "\"set_priority\":{\"priority\":150}}},\"version\":2,\"modified_date_in_millis\":2}")); } + private IndexMetadata buildIndexMetadata(String policy, LifecycleExecutionState.Builder lifecycleState) { + return IndexMetadata.builder("index") + .settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policy)) + .numberOfShards(randomIntBetween(1, 5)) + .numberOfReplicas(randomIntBetween(0, 5)) + .putCustom(ILM_CUSTOM_METADATA_KEY, lifecycleState.build().asMap()) + .build(); + } + private static IndexMetadata.Builder mkMeta() { return IndexMetadata.builder(index) .settings(Settings.builder() @@ -506,4 +534,5 @@ private static IndexMetadata.Builder mkMeta() { .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))); } + } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 21827fd60340f..b2d49f611fe55 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -19,46 +19,32 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; -import org.elasticsearch.xpack.core.ilm.ErrorStep; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; -import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; -import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.Phase; -import org.elasticsearch.xpack.core.ilm.PhaseExecutionInfo; import org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction; -import org.elasticsearch.xpack.core.ilm.Step; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction.Request; -import org.elasticsearch.xpack.ilm.IndexLifecycleTransition; import java.time.Instant; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.SortedMap; -import java.util.Spliterators; import java.util.TreeMap; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; + +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; /** * This class is responsible for bootstrapping {@link IndexLifecycleMetadata} into the cluster-state, as well @@ -141,175 +127,6 @@ public ClusterState execute(ClusterState currentState) throws Exception { }); } - /** - * Ensure that we have the minimum amount of metadata necessary to check for cache phase - * refresh. This includes: - * - An execution state - * - Existing phase definition JSON - * - A current step key - * - A current phase in the step key - * - Not currently in the ERROR step - */ - static boolean eligibleToCheckForRefresh(final IndexMetadata metadata) { - LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(metadata); - if (executionState == null || executionState.getPhaseDefinition() == null) { - return false; - } - - Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(executionState); - if (currentStepKey == null || currentStepKey.getPhase() == null) { - return false; - } - - return ErrorStep.NAME.equals(currentStepKey.getName()) == false; - } - - /** - * Parse the {@code phaseDef} phase definition to get the stepkeys for the given phase. - * If there is an error parsing or if the phase definition is missing the required - * information, returns null. - */ - @Nullable - static Set readStepKeys(final NamedXContentRegistry xContentRegistry, final Client client, - final String phaseDef, final String currentPhase) { - final PhaseExecutionInfo phaseExecutionInfo; - try (XContentParser parser = JsonXContent.jsonXContent.createParser(xContentRegistry, - DeprecationHandler.THROW_UNSUPPORTED_OPERATION, phaseDef)) { - phaseExecutionInfo = PhaseExecutionInfo.parse(parser, currentPhase); - } catch (Exception e) { - logger.trace(new ParameterizedMessage("exception reading step keys checking for refreshability, phase definition: {}", - phaseDef), e); - return null; - } - - if (phaseExecutionInfo == null || phaseExecutionInfo.getPhase() == null) { - return null; - } - - return phaseExecutionInfo.getPhase().getActions().values().stream() - .flatMap(a -> a.toSteps(client, phaseExecutionInfo.getPhase().getName(), null).stream()) - .map(Step::getKey) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - /** - * Returns 'true' if the index's cached phase JSON can be safely reread, 'false' otherwise. - */ - static boolean isIndexPhaseDefinitionUpdatable(final NamedXContentRegistry xContentRegistry, final Client client, - final IndexMetadata metadata, final LifecyclePolicy newPolicy) { - final String index = metadata.getIndex().getName(); - if (eligibleToCheckForRefresh(metadata) == false) { - logger.debug("[{}] does not contain enough information to check for eligibility of refreshing phase", index); - return false; - } - final String policyId = newPolicy.getName(); - - final LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(metadata); - final Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(executionState); - final String currentPhase = currentStepKey.getPhase(); - - final Set newStepKeys = newPolicy.toSteps(client).stream() - .map(Step::getKey) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - if (newStepKeys.contains(currentStepKey) == false) { - // The index is on a step that doesn't exist in the new policy, we - // can't safely re-read the JSON - logger.debug("[{}] updated policy [{}] does not contain the current step key [{}], so the policy phase will not be refreshed", - index, policyId, currentStepKey); - return false; - } - - final String phaseDef = executionState.getPhaseDefinition(); - final Set oldStepKeys = readStepKeys(xContentRegistry, client, phaseDef, currentPhase); - if (oldStepKeys == null) { - logger.debug("[{}] unable to parse phase definition for cached policy [{}], policy phase will not be refreshed", - index, policyId); - return false; - } - - final Set oldPhaseStepKeys = oldStepKeys.stream() - .filter(sk -> currentPhase.equals(sk.getPhase())) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - final PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(policyId, newPolicy.getPhases().get(currentPhase), 1L, 1L); - final String peiJson = Strings.toString(phaseExecutionInfo); - - final Set newPhaseStepKeys = readStepKeys(xContentRegistry, client, peiJson, currentPhase); - if (newPhaseStepKeys == null) { - logger.debug(new ParameterizedMessage("[{}] unable to parse phase definition for policy [{}] " + - "to determine if it could be refreshed", index, policyId)); - return false; - } - - if (newPhaseStepKeys.equals(oldPhaseStepKeys)) { - // The new and old phase have the same stepkeys for this current phase, so we can - // refresh the definition because we know it won't change the execution flow. - logger.debug("[{}] updated policy [{}] contains the same phase step keys and can be refreshed", index, policyId); - return true; - } else { - logger.debug("[{}] updated policy [{}] has different phase step keys and will NOT refresh phase " + - "definition as it differs too greatly. old: {}, new: {}", - index, policyId, oldPhaseStepKeys, newPhaseStepKeys); - return false; - } - } - - /** - * Rereads the phase JSON for the given index, returning a new cluster state. - */ - static ClusterState refreshPhaseDefinition(final ClusterState state, final String index, final LifecyclePolicyMetadata updatedPolicy) { - final IndexMetadata idxMeta = state.metadata().index(index); - assert eligibleToCheckForRefresh(idxMeta) : "index " + index + " is missing crucial information needed to refresh phase definition"; - - logger.trace("[{}] updating cached phase definition for policy [{}]", index, updatedPolicy.getName()); - LifecycleExecutionState currentExState = LifecycleExecutionState.fromIndexMetadata(idxMeta); - - String currentPhase = currentExState.getPhase(); - PhaseExecutionInfo pei = new PhaseExecutionInfo(updatedPolicy.getName(), - updatedPolicy.getPolicy().getPhases().get(currentPhase), updatedPolicy.getVersion(), updatedPolicy.getModifiedDate()); - - LifecycleExecutionState newExState = LifecycleExecutionState.builder(currentExState) - .setPhaseDefinition(Strings.toString(pei, false, false)) - .build(); - - return IndexLifecycleTransition.newClusterStateWithLifecycleState(idxMeta.getIndex(), state, newExState).build(); - } - - /** - * For the given new policy, returns a new cluster with all updateable indices' phase JSON refreshed. - */ - static ClusterState updateIndicesForPolicy(final ClusterState state, final NamedXContentRegistry xContentRegistry, final Client client, - final LifecyclePolicy oldPolicy, final LifecyclePolicyMetadata newPolicy) { - assert oldPolicy.getName().equals(newPolicy.getName()) : "expected both policies to have the same id but they were: [" + - oldPolicy.getName() + "] vs. [" + newPolicy.getName() + "]"; - - // No need to update anything if the policies are identical in contents - if (oldPolicy.equals(newPolicy.getPolicy())) { - logger.debug("policy [{}] is unchanged and no phase definition refresh is needed", oldPolicy.getName()); - return state; - } - - final List indicesThatCanBeUpdated = - StreamSupport.stream(Spliterators.spliteratorUnknownSize(state.metadata().indices().valuesIt(), 0), false) - .filter(meta -> newPolicy.getName().equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(meta.getSettings()))) - .filter(meta -> isIndexPhaseDefinitionUpdatable(xContentRegistry, client, meta, newPolicy.getPolicy())) - .map(meta -> meta.getIndex().getName()) - .collect(Collectors.toList()); - - ClusterState updatedState = state; - for (String index : indicesThatCanBeUpdated) { - try { - updatedState = refreshPhaseDefinition(updatedState, index, newPolicy); - } catch (Exception e) { - logger.warn(new ParameterizedMessage("[{}] unable to refresh phase definition for updated policy [{}]", - index, newPolicy.getName()), e); - } - } - - return updatedState; - } - @Override protected ClusterBlockException checkBlock(Request request, ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java index d1221313897b5..98d091c22c65d 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java @@ -36,6 +36,7 @@ import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.ilm.Phase; import org.elasticsearch.xpack.core.ilm.RolloverAction; +import org.elasticsearch.xpack.core.ilm.SetPriorityAction; import org.elasticsearch.xpack.core.ilm.Step; import java.io.IOException; @@ -48,8 +49,10 @@ import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; -import static org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils.newTestLifecyclePolicy; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.eligibleToCheckForRefresh; +import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.refreshPhaseDefinition; import static org.elasticsearch.xpack.ilm.IndexLifecycleRunnerTests.createOneStepPolicyStepRegistry; +import static org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils.newTestLifecyclePolicy; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -640,6 +643,144 @@ public void testMoveClusterStateToPreviouslyFailedStepAsAutomaticRetry() { assertThat(executionState.getFailedStepRetryCount(), is(1)); } + public void testRefreshPhaseJson() { + LifecycleExecutionState.Builder exState = LifecycleExecutionState.builder() + .setPhase("hot") + .setAction("rollover") + .setStep("check-rollover-ready") + .setPhaseDefinition("{\n" + + " \"policy\" : \"my-policy\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"20m\",\n" + + " \"actions\" : {\n" + + " \"rollover\" : {\n" + + " \"max_age\" : \"5s\"\n" + + " },\n" + + " \"set_priority\" : {\n" + + " \"priority\" : 150\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }"); + + IndexMetadata meta = buildIndexMetadata("my-policy", exState); + String index = meta.getIndex().getName(); + + Map actions = new HashMap<>(); + actions.put("rollover", new RolloverAction(null, null, null, 1L)); + actions.put("set_priority", new SetPriorityAction(100)); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); + Map phases = Collections.singletonMap("hot", hotPhase); + LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + + ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) + .metadata(Metadata.builder(Metadata.EMPTY_METADATA) + .put(meta, false) + .build()) + .build(); + + ClusterState changedState = refreshPhaseDefinition(existingState, index, policyMetadata); + + IndexMetadata newIdxMeta = changedState.metadata().index(index); + LifecycleExecutionState afterExState = LifecycleExecutionState.fromIndexMetadata(newIdxMeta); + Map beforeState = new HashMap<>(exState.build().asMap()); + beforeState.remove("phase_definition"); + Map afterState = new HashMap<>(afterExState.asMap()); + afterState.remove("phase_definition"); + // Check that no other execution state changes have been made + assertThat(beforeState, equalTo(afterState)); + + // Check that the phase definition has been refreshed + assertThat(afterExState.getPhaseDefinition(), + equalTo("{\"policy\":\"my-policy\",\"phase_definition\":{\"min_age\":\"0ms\",\"actions\":{\"rollover\":{\"max_docs\":1}," + + "\"set_priority\":{\"priority\":100}}},\"version\":2,\"modified_date_in_millis\":2}")); + } + + public void testEligibleForRefresh() { + IndexMetadata meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); + + LifecycleExecutionState state = LifecycleExecutionState.builder().build(); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); + + state = LifecycleExecutionState.builder() + .setPhase("phase") + .setAction("action") + .setStep("step") + .build(); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); + + state = LifecycleExecutionState.builder() + .setPhaseDefinition("{}") + .build(); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); + + state = LifecycleExecutionState.builder() + .setPhase("phase") + .setAction("action") + .setStep(ErrorStep.NAME) + .setPhaseDefinition("{}") + .build(); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertFalse(eligibleToCheckForRefresh(meta)); + + state = LifecycleExecutionState.builder() + .setPhase("phase") + .setAction("action") + .setStep("step") + .setPhaseDefinition("{}") + .build(); + meta = IndexMetadata.builder("index") + .settings(Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_INDEX_UUID, randomAlphaOfLength(5))) + .putCustom(ILM_CUSTOM_METADATA_KEY, state.asMap()) + .build(); + assertTrue(eligibleToCheckForRefresh(meta)); + } + private static LifecyclePolicy createPolicy(String policyName, Step.StepKey safeStep, Step.StepKey unsafeStep) { Map phases = new HashMap<>(); if (safeStep != null) { From 9f9a64fc9cbf30d66893d9e3eaaced86180bcb35 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 2 Jun 2021 18:07:09 +0100 Subject: [PATCH 02/19] Fix line length --- .../MetadataMigrateToDataTiersRoutingServiceTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 57ec10d4f18fc..5dd6d380fdff6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -212,8 +212,10 @@ public void testMigrateIndices() { // since the index has a _tier_preference configuration the migrated index should still contain it and have the `data` // attribute routing removed IndexMetadata.Builder indexWithTierPreferenceAndDataAttribute = - IndexMetadata.builder("indexWithTierPreferenceAndDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_REQUIRE_SETTING, - "cold").put(INDEX_ROUTING_PREFER, "data_warm,data_hot")); + IndexMetadata.builder("indexWithTierPreferenceAndDataAttribute").settings(getBaseIndexSettings() + .put(DATA_ROUTING_REQUIRE_SETTING, "cold") + .put(INDEX_ROUTING_PREFER, "data_warm,data_hot") + ); ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithTierPreferenceAndDataAttribute)).build(); From fc0468265c0ba8c732aee5fe7c00391f8d127b63 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 2 Jun 2021 18:32:14 +0100 Subject: [PATCH 03/19] Javadoc mention we update the cached phase definition --- .../metadata/MetadataMigrateToDataTiersRoutingService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index aaf98ced263b6..6c65329ce9a6c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -57,6 +57,7 @@ private MetadataMigrateToDataTiersRoutingService() { * Migrates the elasticsearch abstractions to use data tiers for allocation routing. * This will: * - remove the given V1 index template if it exists. + * * - loop through the existing ILM policies and look at the configured {@link AllocateAction}s. If they define *any* routing rules * based on the provided node attribute name (we look at include, exclude and require rules) *ALL* the rules in the allocate action * will be removed. All the rules are removed in order to allow for ILM to inject the {@link MigrateAction}. @@ -71,6 +72,8 @@ private MetadataMigrateToDataTiersRoutingService() { * number_of_replicas: 0 * } * Note that if the `allocate` action doesn't define any `number_of_replicas` it will be removed completely from the migrated policy. + * As part of migrating the ILM policies we also update the cached phase definition for the managed indices to reflect the migrated + * policy phase. * * - loop through all the indices convert the index.routing.allocation.require.{nodeAttrName} setting (if present) to the * corresponding data tier `_tier_preference` routing. We are only able to convert the `frozen`, `cold`, `warm`, or `hot` setting From e41c59932e8200bc90b4db9d638f51013c527728 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 3 Jun 2021 13:59:14 +0100 Subject: [PATCH 04/19] Add validation that ILM is STOPPED --- ...adataMigrateToDataTiersRoutingService.java | 8 +++- ...MigrateToDataTiersRoutingServiceTests.java | 41 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 6c65329ce9a6c..81e2a2c3dd2cc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -40,6 +40,7 @@ import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; +import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; /** @@ -89,8 +90,13 @@ public static Tuple migrateToDataTiersRouting(Cl @Nullable String nodeAttrName, @Nullable String indexTemplateToDelete, NamedXContentRegistry xContentRegistry, Client client) { - Metadata.Builder mb = Metadata.builder(currentState.metadata()); + IndexLifecycleMetadata currentMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); + if (currentMetadata != null && currentMetadata.getOperationMode() != STOPPED) { + throw new IllegalStateException("stop ILM before migrating to data tiers, current state is [" + + currentMetadata.getOperationMode() + "]"); + } + Metadata.Builder mb = Metadata.builder(currentState.metadata()); String removedIndexTemplateName = null; if (Strings.isNullOrEmpty(indexTemplateToDelete) == false && currentState.metadata().getTemplates().containsKey(indexTemplateToDelete)) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 5dd6d380fdff6..f6adaa4528de1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -46,6 +46,7 @@ import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -84,7 +85,7 @@ public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.RUNNING)) + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) .put(IndexMetadata.builder(indexName).settings(getBaseIndexSettings())).build()) .build(); @@ -141,7 +142,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.RUNNING)) + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) .put(indexMetadata).build()) .build(); @@ -336,7 +337,7 @@ public void testMigrateToDataTiersRouting() { ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( Map.of(policyToMigrate.getName(), policyWithDataAttribute, shouldntBeMigratedPolicy.getName(), policyWithOtherAttribute), - OperationMode.RUNNING)) + OperationMode.STOPPED)) .put(IndexTemplateMetadata.builder("catch-all").patterns(List.of("*")) .settings(Settings.builder().put(DATA_ROUTING_REQUIRE_SETTING, "hot")) .build()) @@ -397,6 +398,40 @@ public void testMigrateToDataTiersRouting() { } } + public void testMigrateToDataTiersRoutingRequiresILMStopped() { + { + ClusterState ilmRunningState = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Map.of(), OperationMode.RUNNING))) + .build(); + IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, + () -> migrateToDataTiersRouting(ilmRunningState, "data", "catch-all", REGISTRY, client)); + assertThat(illegalStateException.getMessage(), is("stop ILM before migrating to data tiers, current state is [RUNNING]")); + } + + { + ClusterState ilmStoppingState = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Map.of(), OperationMode.STOPPING))) + .build(); + IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, + () -> migrateToDataTiersRouting(ilmStoppingState, "data", "catch-all", REGISTRY, client)); + assertThat(illegalStateException.getMessage(), is("stop ILM before migrating to data tiers, current state is [STOPPING]")); + } + + { + ClusterState ilmStoppedState = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Map.of(), OperationMode.STOPPED))) + .build(); + Tuple migratedState = migrateToDataTiersRouting(ilmStoppedState, "data", "catch-all", + REGISTRY, client); + assertThat(migratedState.v2().migratedIndices, empty()); + assertThat(migratedState.v2().migratedPolicies, empty()); + assertThat(migratedState.v2().removedIndexTemplateName, nullValue()); + } + } + private LifecyclePolicyMetadata getWarmColdPolicyMeta(ShrinkAction shrinkAction, AllocateAction warmAllocateAction, AllocateAction coldAllocateAction) { LifecyclePolicy policy = new LifecyclePolicy(lifecycleName, From 405f1b0fcbcb1a5f5241b5f49ed1e883abc31f8f Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 3 Jun 2021 14:13:47 +0100 Subject: [PATCH 05/19] Test migration doesn't delete composable templates --- ...aMigrateToDataTiersRoutingServiceTests.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index f6adaa4528de1..a9e70253bf61d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -11,9 +11,11 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexTemplateMetadata; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; @@ -432,6 +434,22 @@ public void testMigrateToDataTiersRoutingRequiresILMStopped() { } } + public void testMigrationDoesNotRemoveComposableTemplates() { + ComposableIndexTemplate composableIndexTemplate = new ComposableIndexTemplate.Builder() + .indexPatterns(Collections.singletonList("*")) + .template(new Template(Settings.builder().put(DATA_ROUTING_REQUIRE_SETTING, "hot").build(), null, null)) + .build(); + + String composableTemplateName = "catch-all-composable-template"; + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .put(composableTemplateName, composableIndexTemplate).build()) + .build(); + Tuple migratedEntitiesTuple = + migrateToDataTiersRouting(clusterState, "data", composableTemplateName, REGISTRY, client); + assertThat(migratedEntitiesTuple.v2().removedIndexTemplateName, nullValue()); + assertThat(migratedEntitiesTuple.v1().metadata().templatesV2().get(composableTemplateName), is(composableIndexTemplate)); + } + private LifecyclePolicyMetadata getWarmColdPolicyMeta(ShrinkAction shrinkAction, AllocateAction warmAllocateAction, AllocateAction coldAllocateAction) { LifecyclePolicy policy = new LifecyclePolicy(lifecycleName, From 33bd70acdf901e85576c42ff481b125c5db56e8b Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 8 Jun 2021 10:53:30 +0100 Subject: [PATCH 06/19] Use Strings.hasText Co-authored-by: Lee Hinman --- .../metadata/MetadataMigrateToDataTiersRoutingService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 81e2a2c3dd2cc..003b84094cde5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -98,7 +98,7 @@ public static Tuple migrateToDataTiersRouting(Cl Metadata.Builder mb = Metadata.builder(currentState.metadata()); String removedIndexTemplateName = null; - if (Strings.isNullOrEmpty(indexTemplateToDelete) == false && + if (Strings.hasText(indexTemplateToDelete) && currentState.metadata().getTemplates().containsKey(indexTemplateToDelete)) { mb.removeTemplate(indexTemplateToDelete); logger.debug("removing template [{}]", indexTemplateToDelete); From fba9f611c02649aa208d7ffc07666b4bb5a4680f Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 8 Jun 2021 10:55:34 +0100 Subject: [PATCH 07/19] Flip ILM metadata check --- ...adataMigrateToDataTiersRoutingService.java | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 003b84094cde5..03f02564fa3a5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -117,60 +117,62 @@ public static Tuple migrateToDataTiersRouting(Cl static List migrateIlmPolicies(Metadata.Builder mb, ClusterState currentState, String nodeAttrName, NamedXContentRegistry xContentRegistry, Client client) { - List migratedPolicies = new ArrayList<>(); IndexLifecycleMetadata currentLifecycleMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); - if (currentLifecycleMetadata != null) { - Map currentPolicies = currentLifecycleMetadata.getPolicyMetadatas(); - SortedMap newPolicies = new TreeMap<>(currentPolicies); - for (Map.Entry policyMetadataEntry : currentPolicies.entrySet()) { - LifecyclePolicy lifecyclePolicy = policyMetadataEntry.getValue().getPolicy(); - LifecyclePolicy newLifecylePolicy = null; - for (Map.Entry phaseEntry : lifecyclePolicy.getPhases().entrySet()) { - Phase phase = phaseEntry.getValue(); - AllocateAction allocateAction = (AllocateAction) phase.getActions().get(AllocateAction.NAME); - if (allocateActionDefinesRoutingRules(nodeAttrName, allocateAction)) { - Map actionMap = new HashMap<>(phase.getActions()); - // this phase contains an allocate action that defines a require rule for the attribute name so we'll remove all the - // rules to allow for the migrate action to be injected - if (allocateAction.getNumberOfReplicas() != null) { - // keep the number of replicas configuration - AllocateAction updatedAllocateAction = - new AllocateAction(allocateAction.getNumberOfReplicas(), null, null, null); - actionMap.put(allocateAction.getWriteableName(), updatedAllocateAction); - logger.debug("ILM policy [{}], phase [{}]: updated the allocate action to [{}]", lifecyclePolicy.getName(), - phase.getName(), allocateAction); - } else { - // remove the action altogether - actionMap.remove(allocateAction.getWriteableName()); - logger.debug("ILM policy [{}], phase [{}]: removed the allocate action", lifecyclePolicy.getName(), - phase.getName()); - } + if (currentLifecycleMetadata == null) { + return Collections.emptyList(); + } - Phase updatedPhase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap); - Map updatedPhases = - new HashMap<>(newLifecylePolicy == null ? lifecyclePolicy.getPhases() : newLifecylePolicy.getPhases()); - updatedPhases.put(phaseEntry.getKey(), updatedPhase); - newLifecylePolicy = new LifecyclePolicy(lifecyclePolicy.getName(), updatedPhases); + List migratedPolicies = new ArrayList<>(); + Map currentPolicies = currentLifecycleMetadata.getPolicyMetadatas(); + SortedMap newPolicies = new TreeMap<>(currentPolicies); + for (Map.Entry policyMetadataEntry : currentPolicies.entrySet()) { + LifecyclePolicy lifecyclePolicy = policyMetadataEntry.getValue().getPolicy(); + LifecyclePolicy newLifecylePolicy = null; + for (Map.Entry phaseEntry : lifecyclePolicy.getPhases().entrySet()) { + Phase phase = phaseEntry.getValue(); + AllocateAction allocateAction = (AllocateAction) phase.getActions().get(AllocateAction.NAME); + if (allocateActionDefinesRoutingRules(nodeAttrName, allocateAction)) { + Map actionMap = new HashMap<>(phase.getActions()); + // this phase contains an allocate action that defines a require rule for the attribute name so we'll remove all the + // rules to allow for the migrate action to be injected + if (allocateAction.getNumberOfReplicas() != null) { + // keep the number of replicas configuration + AllocateAction updatedAllocateAction = + new AllocateAction(allocateAction.getNumberOfReplicas(), null, null, null); + actionMap.put(allocateAction.getWriteableName(), updatedAllocateAction); + logger.debug("ILM policy [{}], phase [{}]: updated the allocate action to [{}]", lifecyclePolicy.getName(), + phase.getName(), allocateAction); + } else { + // remove the action altogether + actionMap.remove(allocateAction.getWriteableName()); + logger.debug("ILM policy [{}], phase [{}]: removed the allocate action", lifecyclePolicy.getName(), + phase.getName()); } - } - if (newLifecylePolicy != null) { - // we updated at least one phase - long nextVersion = policyMetadataEntry.getValue().getVersion() + 1L; - LifecyclePolicyMetadata newPolicyMetadata = new LifecyclePolicyMetadata(newLifecylePolicy, - policyMetadataEntry.getValue().getHeaders(), nextVersion, Instant.now().toEpochMilli()); - LifecyclePolicyMetadata oldPolicyMetadata = newPolicies.put(policyMetadataEntry.getKey(), newPolicyMetadata); - assert oldPolicyMetadata != null : - "we must only update policies, not create new ones, but " + policyMetadataEntry.getKey() + " didn't exist"; - - updateIndicesForPolicy(mb, currentState, xContentRegistry, client, oldPolicyMetadata.getPolicy(), newPolicyMetadata); - migratedPolicies.add(policyMetadataEntry.getKey()); + Phase updatedPhase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap); + Map updatedPhases = + new HashMap<>(newLifecylePolicy == null ? lifecyclePolicy.getPhases() : newLifecylePolicy.getPhases()); + updatedPhases.put(phaseEntry.getKey(), updatedPhase); + newLifecylePolicy = new LifecyclePolicy(lifecyclePolicy.getName(), updatedPhases); } } - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); - mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); + if (newLifecylePolicy != null) { + // we updated at least one phase + long nextVersion = policyMetadataEntry.getValue().getVersion() + 1L; + LifecyclePolicyMetadata newPolicyMetadata = new LifecyclePolicyMetadata(newLifecylePolicy, + policyMetadataEntry.getValue().getHeaders(), nextVersion, Instant.now().toEpochMilli()); + LifecyclePolicyMetadata oldPolicyMetadata = newPolicies.put(policyMetadataEntry.getKey(), newPolicyMetadata); + assert oldPolicyMetadata != null : + "we must only update policies, not create new ones, but " + policyMetadataEntry.getKey() + " didn't exist"; + + updateIndicesForPolicy(mb, currentState, xContentRegistry, client, oldPolicyMetadata.getPolicy(), newPolicyMetadata); + migratedPolicies.add(policyMetadataEntry.getKey()); + } } + + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); + mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); return migratedPolicies; } From 83ce6ac68ee4cc3ef516b754b100389aa993f707 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 8 Jun 2021 13:11:24 +0100 Subject: [PATCH 08/19] Inspect the `include.data` node attribute routing too --- ...adataMigrateToDataTiersRoutingService.java | 88 +++++++++++-------- ...MigrateToDataTiersRoutingServiceTests.java | 48 +++++++++- 2 files changed, 97 insertions(+), 39 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 03f02564fa3a5..d37a407b42500 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -38,6 +38,7 @@ import java.util.SortedMap; import java.util.TreeMap; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; @@ -76,9 +77,10 @@ private MetadataMigrateToDataTiersRoutingService() { * As part of migrating the ILM policies we also update the cached phase definition for the managed indices to reflect the migrated * policy phase. * - * - loop through all the indices convert the index.routing.allocation.require.{nodeAttrName} setting (if present) to the - * corresponding data tier `_tier_preference` routing. We are only able to convert the `frozen`, `cold`, `warm`, or `hot` setting - * values to the `_tier_preference`. If other configuration values are present eg ("the_warm_nodes") the index will not be migrated. + * - loop through all the indices convert the index.routing.allocation.require.{nodeAttrName} or + * index.routing.allocation.include.{nodeAttrName} setting (if present) to the corresponding data tier `_tier_preference` routing. + * We are only able to convert the `frozen`, `cold`, `warm`, or `hot` setting values to the `_tier_preference`. If other + * configuration values are present eg ("the_warm_nodes") the index will not be migrated. * * If no @param nodeAttrName is provided "data" will be used. * If no @param indexTemplateToDelete is provided, no index templates will be deleted. @@ -184,48 +186,60 @@ static boolean allocateActionDefinesRoutingRules(String nodeAttrName, @Nullable static List migrateIndices(Metadata.Builder mb, ClusterState currentState, String nodeAttrName) { List migratedIndices = new ArrayList<>(); - String nodeAttrIndexRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName; + String nodeAttrIndexRequireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName; + String nodeAttrIndexIncludeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName; for (ObjectObjectCursor index : currentState.metadata().indices()) { IndexMetadata indexMetadata = index.value; - Settings currentIndexSettings = indexMetadata.getSettings(); - if (currentIndexSettings.keySet().contains(nodeAttrIndexRoutingSetting)) { - // look at the value, get the correct tiers config and update the settings and index metadata - Settings.Builder newSettingsBuilder = Settings.builder().put(currentIndexSettings); - String indexName = indexMetadata.getIndex().getName(); - if (currentIndexSettings.keySet().contains(INDEX_ROUTING_PREFER) == false) { - // parse the custom attribute routing into the corresponding tier preference and configure it - String attributeValue = currentIndexSettings.get(nodeAttrIndexRoutingSetting); - String convertedTierPreference = convertAttributeValueToTierPreference(attributeValue); - if (convertedTierPreference != null) { - newSettingsBuilder.put(INDEX_ROUTING_PREFER, convertedTierPreference); - newSettingsBuilder.remove(nodeAttrIndexRoutingSetting); - logger.debug("index [{}]: removed setting [{}]", indexName, nodeAttrIndexRoutingSetting); - logger.debug("index [{}]: configured setting [{}] to [{}]", indexName, - INDEX_ROUTING_PREFER, convertedTierPreference); - } else { - // log warning and do *not* remove setting - logger.warn("index [{}]: could not convert attribute based setting [{}] value of [{}] to a tier preference " + - "configuration. the only known values are: {}", indexName, - nodeAttrIndexRoutingSetting, attributeValue, "hot,warm,cold, and frozen"); - continue; - } - } else { - newSettingsBuilder.remove(nodeAttrIndexRoutingSetting); - logger.debug("index [{}]: removed setting [{}]", indexName, nodeAttrIndexRoutingSetting); - } + Settings currentSettings = indexMetadata.getSettings(); + Settings newSettings = migrateIndexSettings(nodeAttrIndexRequireRoutingSetting, indexMetadata); + if (newSettings.equals(currentSettings)) { + // migrating based on the `require` setting was not successful so let's check if the index used the `include` routing + // setting to configure the allocations and try to migrate it + newSettings = migrateIndexSettings(nodeAttrIndexIncludeRoutingSetting, indexMetadata); + } - Settings newSettings = newSettingsBuilder.build(); - if (currentIndexSettings.equals(newSettings) == false) { - mb.put(IndexMetadata.builder(indexMetadata) - .settings(newSettings) - .settingsVersion(indexMetadata.getSettingsVersion() + 1)); - migratedIndices.add(indexName); - } + if (newSettings.equals(currentSettings) == false) { + mb.put(IndexMetadata.builder(indexMetadata) + .settings(newSettings) + .settingsVersion(indexMetadata.getSettingsVersion() + 1)); + migratedIndices.add(indexMetadata.getIndex().getName()); } } return migratedIndices; } + private static Settings migrateIndexSettings(String attributeBasedRoutingSettingName, IndexMetadata indexMetadata) { + Settings currentIndexSettings = indexMetadata.getSettings(); + if (currentIndexSettings.keySet().contains(attributeBasedRoutingSettingName) == false) { + return currentIndexSettings; + } + // look at the value, get the correct tiers config and update the settings and index metadata + Settings.Builder newSettingsBuilder = Settings.builder().put(currentIndexSettings); + String indexName = indexMetadata.getIndex().getName(); + if (currentIndexSettings.keySet().contains(INDEX_ROUTING_PREFER)) { + newSettingsBuilder.remove(attributeBasedRoutingSettingName); + logger.debug("index [{}]: removed setting [{}]", indexName, attributeBasedRoutingSettingName); + } else { + // parse the custom attribute routing into the corresponding tier preference and configure it + String attributeValue = currentIndexSettings.get(attributeBasedRoutingSettingName); + String convertedTierPreference = convertAttributeValueToTierPreference(attributeValue); + if (convertedTierPreference != null) { + newSettingsBuilder.put(INDEX_ROUTING_PREFER, convertedTierPreference); + newSettingsBuilder.remove(attributeBasedRoutingSettingName); + logger.debug("index [{}]: removed setting [{}]", indexName, attributeBasedRoutingSettingName); + logger.debug("index [{}]: configured setting [{}] to [{}]", indexName, + INDEX_ROUTING_PREFER, convertedTierPreference); + } else { + // log warning and do *not* remove setting, return the settings unchanged + logger.warn("index [{}]: could not convert attribute based setting [{}] value of [{}] to a tier preference " + + "configuration. the only known values are: {}", indexName, + attributeBasedRoutingSettingName, attributeValue, "hot,warm,cold, and frozen"); + return currentIndexSettings; + } + } + return newSettingsBuilder.build(); + } + /** * Converts the provided node attribute value to the corresponding `_tier_preference` configuration. * Known (and convertible) attribute values are: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index a9e70253bf61d..061baa364a147 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.allocateActionDefinesRoutingRules; import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.convertAttributeValueToTierPreference; @@ -57,6 +58,7 @@ public class MetadataMigrateToDataTiersRoutingServiceTests extends ESTestCase { private static final String DATA_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "data"; + private static final String DATA_ROUTING_INCLUDE_SETTING = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + "data"; private static final String BOX_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "box"; private static final NamedXContentRegistry REGISTRY; @@ -163,12 +165,11 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { } private Settings.Builder getBaseIndexSettings() { - Settings.Builder settings = Settings.builder() + return Settings.builder() .put(LifecycleSettings.LIFECYCLE_NAME, lifecycleName) .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 10)) .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 5)) .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT); - return settings; } public void testAllocateActionDefinesRoutingRules() { @@ -211,6 +212,26 @@ public void testMigrateIndices() { assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); } + { + // test the migration of the `include.data` configuration to the equivalent _tier_preference routing + IndexMetadata.Builder indexWitWarmDataAttribute = + IndexMetadata.builder("indexWitWarmDataAttribute").settings(getBaseIndexSettings().put(DATA_ROUTING_INCLUDE_SETTING, + "warm")); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWitWarmDataAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWitWarmDataAttribute")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWitWarmDataAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + { // since the index has a _tier_preference configuration the migrated index should still contain it and have the `data` // attribute routing removed @@ -234,6 +255,29 @@ public void testMigrateIndices() { assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); } + { + // like above, test a combination of node attribute and _tier_preference routings configured for the original index, but this + // time using the `include.data` setting + IndexMetadata.Builder indexWithTierPreferenceAndDataAttribute = + IndexMetadata.builder("indexWithTierPreferenceAndDataAttribute").settings(getBaseIndexSettings() + .put(DATA_ROUTING_INCLUDE_SETTING, "cold") + .put(INDEX_ROUTING_PREFER, "data_warm,data_hot") + ); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithTierPreferenceAndDataAttribute)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWithTierPreferenceAndDataAttribute")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithTierPreferenceAndDataAttribute"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + { // index with an unknown `data` attribute routing value should **not** be migrated IndexMetadata.Builder indexWithUnknownDataAttribute = From a25a4875fd1e19f90484d647413748a0a3a3224f Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 8 Jun 2021 13:18:06 +0100 Subject: [PATCH 09/19] Extract `migrateSingleILMPolicy` method --- ...adataMigrateToDataTiersRoutingService.java | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index d37a407b42500..e7f0a65c41a89 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -128,41 +128,11 @@ static List migrateIlmPolicies(Metadata.Builder mb, ClusterState current Map currentPolicies = currentLifecycleMetadata.getPolicyMetadatas(); SortedMap newPolicies = new TreeMap<>(currentPolicies); for (Map.Entry policyMetadataEntry : currentPolicies.entrySet()) { - LifecyclePolicy lifecyclePolicy = policyMetadataEntry.getValue().getPolicy(); - LifecyclePolicy newLifecylePolicy = null; - for (Map.Entry phaseEntry : lifecyclePolicy.getPhases().entrySet()) { - Phase phase = phaseEntry.getValue(); - AllocateAction allocateAction = (AllocateAction) phase.getActions().get(AllocateAction.NAME); - if (allocateActionDefinesRoutingRules(nodeAttrName, allocateAction)) { - Map actionMap = new HashMap<>(phase.getActions()); - // this phase contains an allocate action that defines a require rule for the attribute name so we'll remove all the - // rules to allow for the migrate action to be injected - if (allocateAction.getNumberOfReplicas() != null) { - // keep the number of replicas configuration - AllocateAction updatedAllocateAction = - new AllocateAction(allocateAction.getNumberOfReplicas(), null, null, null); - actionMap.put(allocateAction.getWriteableName(), updatedAllocateAction); - logger.debug("ILM policy [{}], phase [{}]: updated the allocate action to [{}]", lifecyclePolicy.getName(), - phase.getName(), allocateAction); - } else { - // remove the action altogether - actionMap.remove(allocateAction.getWriteableName()); - logger.debug("ILM policy [{}], phase [{}]: removed the allocate action", lifecyclePolicy.getName(), - phase.getName()); - } - - Phase updatedPhase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap); - Map updatedPhases = - new HashMap<>(newLifecylePolicy == null ? lifecyclePolicy.getPhases() : newLifecylePolicy.getPhases()); - updatedPhases.put(phaseEntry.getKey(), updatedPhase); - newLifecylePolicy = new LifecyclePolicy(lifecyclePolicy.getName(), updatedPhases); - } - } - - if (newLifecylePolicy != null) { + LifecyclePolicy newLifecyclePolicy = migrateSingleILMPolicy(nodeAttrName, policyMetadataEntry.getValue().getPolicy()); + if (newLifecyclePolicy != null) { // we updated at least one phase long nextVersion = policyMetadataEntry.getValue().getVersion() + 1L; - LifecyclePolicyMetadata newPolicyMetadata = new LifecyclePolicyMetadata(newLifecylePolicy, + LifecyclePolicyMetadata newPolicyMetadata = new LifecyclePolicyMetadata(newLifecyclePolicy, policyMetadataEntry.getValue().getHeaders(), nextVersion, Instant.now().toEpochMilli()); LifecyclePolicyMetadata oldPolicyMetadata = newPolicies.put(policyMetadataEntry.getKey(), newPolicyMetadata); assert oldPolicyMetadata != null : @@ -178,6 +148,40 @@ static List migrateIlmPolicies(Metadata.Builder mb, ClusterState current return migratedPolicies; } + @Nullable + private static LifecyclePolicy migrateSingleILMPolicy(String nodeAttrName, LifecyclePolicy lifecyclePolicy) { + LifecyclePolicy newLifecyclePolicy = null; + for (Map.Entry phaseEntry : lifecyclePolicy.getPhases().entrySet()) { + Phase phase = phaseEntry.getValue(); + AllocateAction allocateAction = (AllocateAction) phase.getActions().get(AllocateAction.NAME); + if (allocateActionDefinesRoutingRules(nodeAttrName, allocateAction)) { + Map actionMap = new HashMap<>(phase.getActions()); + // this phase contains an allocate action that defines a require rule for the attribute name so we'll remove all the + // rules to allow for the migrate action to be injected + if (allocateAction.getNumberOfReplicas() != null) { + // keep the number of replicas configuration + AllocateAction updatedAllocateAction = + new AllocateAction(allocateAction.getNumberOfReplicas(), null, null, null); + actionMap.put(allocateAction.getWriteableName(), updatedAllocateAction); + logger.debug("ILM policy [{}], phase [{}]: updated the allocate action to [{}]", lifecyclePolicy.getName(), + phase.getName(), allocateAction); + } else { + // remove the action altogether + actionMap.remove(allocateAction.getWriteableName()); + logger.debug("ILM policy [{}], phase [{}]: removed the allocate action", lifecyclePolicy.getName(), + phase.getName()); + } + + Phase updatedPhase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap); + Map updatedPhases = + new HashMap<>(newLifecyclePolicy == null ? lifecyclePolicy.getPhases() : newLifecyclePolicy.getPhases()); + updatedPhases.put(phaseEntry.getKey(), updatedPhase); + newLifecyclePolicy = new LifecyclePolicy(lifecyclePolicy.getName(), updatedPhases); + } + } + return newLifecyclePolicy; + } + static boolean allocateActionDefinesRoutingRules(String nodeAttrName, @Nullable AllocateAction allocateAction) { return allocateAction != null && (allocateAction.getRequire().get(nodeAttrName) != null || allocateAction.getInclude().get(nodeAttrName) != null || From 90a81ed568636a35e77d9d4c414dd1b0b4f8ddb9 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 8 Jun 2021 13:25:01 +0100 Subject: [PATCH 10/19] Add test to check the require setting is migrated even if include is present --- ...MigrateToDataTiersRoutingServiceTests.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 061baa364a147..ba0a1c7d484ae 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -355,6 +355,30 @@ public void testMigrateIndices() { } } + public void testRequireAttributeIndexSettingTakesPriorityOverInclude() { + IndexMetadata.Builder indexWithRequireAndInclude = + IndexMetadata.builder("indexWithRequireAndInclude") + .settings(getBaseIndexSettings() + .put(DATA_ROUTING_REQUIRE_SETTING, "warm") + .put(DATA_ROUTING_INCLUDE_SETTING, "cold") + ); + ClusterState state = + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithRequireAndInclude)).build(); + + Metadata.Builder mb = Metadata.builder(state.metadata()); + + List migratedIndices = migrateIndices(mb, state, "data"); + assertThat(migratedIndices.size(), is(1)); + assertThat(migratedIndices.get(0), is("indexWithRequireAndInclude")); + + ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithRequireAndInclude"); + assertThat("index migration ONLY clears the setting it migrates, in this case the require.data setting", + migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), is("cold")); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); + } + public void testMigrateToDataTiersRouting() { AllocateAction allocateActionWithDataAttribute = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); AllocateAction allocateActionWithOtherAttribute = new AllocateAction(0, null, null, Map.of("other", "cold")); From 13268b6e4efa1d4e342a1cfcc79dc0186e208cd0 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 10 Jun 2021 14:26:25 +0100 Subject: [PATCH 11/19] Fix master merge --- .../metadata/MetadataMigrateToDataTiersRoutingService.java | 4 ++-- .../elasticsearch/xpack/core/ilm/PhaseCacheManagement.java | 2 +- .../MetadataMigrateToDataTiersRoutingServiceTests.java | 6 +++--- .../xpack/core/ilm/PhaseCacheManagementTests.java | 4 ++-- .../xpack/ilm/action/TransportPutLifecycleAction.java | 2 -- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index e7f0a65c41a89..ca06fed036d84 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -14,11 +14,11 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.DataTier; import org.elasticsearch.xpack.core.ilm.AllocateAction; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java index 05ff3b1552f51..c31dde4083347 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java @@ -14,7 +14,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.common.Nullable; +import org.elasticsearch.core.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index ba0a1c7d484ae..d0131caf77816 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -16,11 +16,11 @@ import org.elasticsearch.cluster.metadata.IndexTemplateMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.Template; -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ParseField; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.MigratedEntities; import org.elasticsearch.xpack.core.ilm.AllocateAction; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java index 0057730984be2..069623a4efffc 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java @@ -12,11 +12,11 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ParseField; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.ESTestCase; import java.util.Collections; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 02c7fae84ad79..b2d49f611fe55 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -22,8 +22,6 @@ import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.license.XPackLicenseState; From f05ef7411eae3fe483e24c47bd334bd5b6da2dce Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 15 Jun 2021 10:41:41 +0100 Subject: [PATCH 12/19] Oxford comma Co-authored-by: Lee Hinman --- .../metadata/MetadataMigrateToDataTiersRoutingService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index ca06fed036d84..5d3228d0e171d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -61,7 +61,7 @@ private MetadataMigrateToDataTiersRoutingService() { * - remove the given V1 index template if it exists. * * - loop through the existing ILM policies and look at the configured {@link AllocateAction}s. If they define *any* routing rules - * based on the provided node attribute name (we look at include, exclude and require rules) *ALL* the rules in the allocate action + * based on the provided node attribute name (we look at include, exclude, and require rules) *ALL* the rules in the allocate action * will be removed. All the rules are removed in order to allow for ILM to inject the {@link MigrateAction}. * So for eg. this action: * allocate { From 9cc1198d2d4f7c1a600ce475566353ea1160d6a3 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 15 Jun 2021 11:34:01 +0100 Subject: [PATCH 13/19] Debug logging when the template doesn't exist --- .../MetadataMigrateToDataTiersRoutingService.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 5d3228d0e171d..4406bed6ead3e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -100,11 +100,14 @@ public static Tuple migrateToDataTiersRouting(Cl Metadata.Builder mb = Metadata.builder(currentState.metadata()); String removedIndexTemplateName = null; - if (Strings.hasText(indexTemplateToDelete) && - currentState.metadata().getTemplates().containsKey(indexTemplateToDelete)) { - mb.removeTemplate(indexTemplateToDelete); - logger.debug("removing template [{}]", indexTemplateToDelete); - removedIndexTemplateName = indexTemplateToDelete; + if (Strings.hasText(indexTemplateToDelete)) { + if (currentState.metadata().getTemplates().containsKey(indexTemplateToDelete)) { + mb.removeTemplate(indexTemplateToDelete); + logger.debug("removing legacy template [{}]", indexTemplateToDelete); + removedIndexTemplateName = indexTemplateToDelete; + } else { + logger.debug("legacy template [{}] does not exist", indexTemplateToDelete); + } } String attribute = nodeAttrName; From 00659d1b05fcd227f9190d75dae4bc2720b9681d Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 15 Jun 2021 11:41:57 +0100 Subject: [PATCH 14/19] Debug log how many indices had their ILM phase definition refreshed --- .../xpack/core/ilm/PhaseCacheManagement.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java index c31dde4083347..ea4930ae6d069 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagement.java @@ -14,13 +14,14 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.core.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.core.Nullable; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -137,16 +138,18 @@ public static boolean updateIndicesForPolicy(final Metadata.Builder mb, final Cl .filter(meta -> isIndexPhaseDefinitionUpdatable(xContentRegistry, client, meta, newPolicy.getPolicy())) .collect(Collectors.toList()); + final List refreshedIndices = new ArrayList<>(indicesThatCanBeUpdated.size()); for (IndexMetadata index : indicesThatCanBeUpdated) { try { refreshPhaseDefinition(mb, index, newPolicy); + refreshedIndices.add(index.getIndex().getName()); } catch (Exception e) { logger.warn(new ParameterizedMessage("[{}] unable to refresh phase definition for updated policy [{}]", index, newPolicy.getName()), e); } } - - return indicesThatCanBeUpdated.size() > 0; + logger.debug("refreshed policy [{}] phase definition for [{}] indices", newPolicy.getName(), refreshedIndices.size()); + return refreshedIndices.size() > 0; } /** From ddd6aa777b4ea12af9e83107bfb0d665e6255aaa Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 15 Jun 2021 14:14:18 +0100 Subject: [PATCH 15/19] Remove all attribute routing settings when indices are migrated --- ...adataMigrateToDataTiersRoutingService.java | 63 +++++++++++++++++-- ...MigrateToDataTiersRoutingServiceTests.java | 21 ++++--- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 4406bed6ead3e..3ad1ca0e5a00e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -38,6 +38,7 @@ import java.util.SortedMap; import java.util.TreeMap; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; @@ -81,6 +82,23 @@ private MetadataMigrateToDataTiersRoutingService() { * index.routing.allocation.include.{nodeAttrName} setting (if present) to the corresponding data tier `_tier_preference` routing. * We are only able to convert the `frozen`, `cold`, `warm`, or `hot` setting values to the `_tier_preference`. If other * configuration values are present eg ("the_warm_nodes") the index will not be migrated. + * If the require or include setting is successfully migrated to _tier_preference, the **other** routing settings for the + * provided attribute are also removed (if present). + * Eg. if we manage to migrate the `index.routing.allocation.require.data` setting, but the index also has configured + * `index.routing.allocation.include.data` and `index.routing.allocation.exclude.data`, the + * migrated settings will contain `index.routing.allocation.include._tier_preference` configured to the corresponding + * `index.routing.allocation.require.data` value, with `index.routing.allocation.include.data` and + * `index.routing.allocation.exclude.data` being removed. + * Settings: + * { + * index.routing.allocation.require.data: "warm", + * index.routing.allocation.include.data: "rack1", + * index.routing.allocation.exclude.data: "rack2,rack3" + * } + * will be migrated to: + * { + * index.routing.allocation.include._tier_preference: "data_warm,data_hot" + * } * * If no @param nodeAttrName is provided "data" will be used. * If no @param indexTemplateToDelete is provided, no index templates will be deleted. @@ -120,6 +138,13 @@ public static Tuple migrateToDataTiersRouting(Cl new MigratedEntities(removedIndexTemplateName, migratedIndices, migratedPolicies)); } + /** + * Iterate through the existing ILM policies and look at the configured {@link AllocateAction}s. If they define *any* routing rules + * based on the provided node attribute name (we look at include, exclude, and require rules) *ALL* the rules in the allocate + * action will be removed. All the rules are removed in order to allow for ILM to inject the {@link MigrateAction}. + * This also iterates through all the indices that are executing a given *migrated* policy and refreshes the cached phase definition + * for each of these managed indices. + */ static List migrateIlmPolicies(Metadata.Builder mb, ClusterState currentState, String nodeAttrName, NamedXContentRegistry xContentRegistry, Client client) { IndexLifecycleMetadata currentLifecycleMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); @@ -151,6 +176,12 @@ static List migrateIlmPolicies(Metadata.Builder mb, ClusterState current return migratedPolicies; } + /** + * Migrates a single ILM policy from defining {@link AllocateAction}s in order to configure shard allocation routing based on the + * provided node attribute name towards allowing ILM to inject the {@link MigrateAction}. + * + * Returns the migrated ILM policy. + */ @Nullable private static LifecyclePolicy migrateSingleILMPolicy(String nodeAttrName, LifecyclePolicy lifecyclePolicy) { LifecyclePolicy newLifecyclePolicy = null; @@ -185,29 +216,45 @@ private static LifecyclePolicy migrateSingleILMPolicy(String nodeAttrName, Lifec return newLifecyclePolicy; } + /** + * Returns true of the provided {@link AllocateAction} defines any index allocation rules. + */ static boolean allocateActionDefinesRoutingRules(String nodeAttrName, @Nullable AllocateAction allocateAction) { return allocateAction != null && (allocateAction.getRequire().get(nodeAttrName) != null || allocateAction.getInclude().get(nodeAttrName) != null || allocateAction.getExclude().get(nodeAttrName) != null); } + /** + * Iterates through the existing indices and migrates them away from using attribute based routing using the provided node + * attribute name towards the tier preference routing. + * Returns a list of the migrated indices. + */ static List migrateIndices(Metadata.Builder mb, ClusterState currentState, String nodeAttrName) { List migratedIndices = new ArrayList<>(); String nodeAttrIndexRequireRoutingSetting = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + nodeAttrName; String nodeAttrIndexIncludeRoutingSetting = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + nodeAttrName; + String nodeAttrIndexExcludeRoutingSetting = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + nodeAttrName; for (ObjectObjectCursor index : currentState.metadata().indices()) { IndexMetadata indexMetadata = index.value; Settings currentSettings = indexMetadata.getSettings(); - Settings newSettings = migrateIndexSettings(nodeAttrIndexRequireRoutingSetting, indexMetadata); + Settings newSettings = maybeMigrateRoutingSettingToTierPreference(nodeAttrIndexRequireRoutingSetting, indexMetadata); if (newSettings.equals(currentSettings)) { // migrating based on the `require` setting was not successful so let's check if the index used the `include` routing // setting to configure the allocations and try to migrate it - newSettings = migrateIndexSettings(nodeAttrIndexIncludeRoutingSetting, indexMetadata); + newSettings = maybeMigrateRoutingSettingToTierPreference(nodeAttrIndexIncludeRoutingSetting, indexMetadata); } if (newSettings.equals(currentSettings) == false) { + // we converted either the require or the include routing setting to tier preference + // so let's clear all the routing settings for the given attribute + Settings.Builder finalSettings = Settings.builder().put(newSettings); + finalSettings.remove(nodeAttrIndexExcludeRoutingSetting); + finalSettings.remove(nodeAttrIndexRequireRoutingSetting); + finalSettings.remove(nodeAttrIndexIncludeRoutingSetting); + mb.put(IndexMetadata.builder(indexMetadata) - .settings(newSettings) + .settings(finalSettings) .settingsVersion(indexMetadata.getSettingsVersion() + 1)); migratedIndices.add(indexMetadata.getIndex().getName()); } @@ -215,7 +262,15 @@ static List migrateIndices(Metadata.Builder mb, ClusterState currentStat return migratedIndices; } - private static Settings migrateIndexSettings(String attributeBasedRoutingSettingName, IndexMetadata indexMetadata) { + /** + * Attempts to migrate the value of the given attribute routing setting to the _tier_preference equivalent. The provided setting + * needs to be configured and have one of the supported values (hot, warm, cold, or frozen) in order for the migration to be preformed. + * If the migration is successful the provided setting will be removed. + * + * If the migration is **not** executed the current index settings is returned, otherwise the updated settings are returned + */ + private static Settings maybeMigrateRoutingSettingToTierPreference(String attributeBasedRoutingSettingName, + IndexMetadata indexMetadata) { Settings currentIndexSettings = indexMetadata.getSettings(); if (currentIndexSettings.keySet().contains(attributeBasedRoutingSettingName) == false) { return currentIndexSettings; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index d0131caf77816..97ea755fa8807 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.allocateActionDefinesRoutingRules; @@ -58,6 +59,7 @@ public class MetadataMigrateToDataTiersRoutingServiceTests extends ESTestCase { private static final String DATA_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "data"; + private static final String DATA_ROUTING_EXCLUDE_SETTING = INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + "data"; private static final String DATA_ROUTING_INCLUDE_SETTING = INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + "data"; private static final String BOX_ROUTING_REQUIRE_SETTING = INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "box"; private static final NamedXContentRegistry REGISTRY; @@ -234,10 +236,11 @@ public void testMigrateIndices() { { // since the index has a _tier_preference configuration the migrated index should still contain it and have the `data` - // attribute routing removed + // attributes routing removed IndexMetadata.Builder indexWithTierPreferenceAndDataAttribute = IndexMetadata.builder("indexWithTierPreferenceAndDataAttribute").settings(getBaseIndexSettings() .put(DATA_ROUTING_REQUIRE_SETTING, "cold") + .put(DATA_ROUTING_INCLUDE_SETTING, "hot") .put(INDEX_ROUTING_PREFER, "data_warm,data_hot") ); ClusterState state = @@ -252,6 +255,7 @@ public void testMigrateIndices() { ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); IndexMetadata migratedIndex = migratedState.metadata().index("indexWithTierPreferenceAndDataAttribute"); assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), nullValue()); assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); } @@ -356,26 +360,27 @@ public void testMigrateIndices() { } public void testRequireAttributeIndexSettingTakesPriorityOverInclude() { - IndexMetadata.Builder indexWithRequireAndInclude = - IndexMetadata.builder("indexWithRequireAndInclude") + IndexMetadata.Builder indexWithAllRoutingSettings = + IndexMetadata.builder("indexWithAllRoutingSettings") .settings(getBaseIndexSettings() .put(DATA_ROUTING_REQUIRE_SETTING, "warm") .put(DATA_ROUTING_INCLUDE_SETTING, "cold") + .put(DATA_ROUTING_EXCLUDE_SETTING, "hot") ); ClusterState state = - ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithRequireAndInclude)).build(); + ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder().put(indexWithAllRoutingSettings)).build(); Metadata.Builder mb = Metadata.builder(state.metadata()); List migratedIndices = migrateIndices(mb, state, "data"); assertThat(migratedIndices.size(), is(1)); - assertThat(migratedIndices.get(0), is("indexWithRequireAndInclude")); + assertThat(migratedIndices.get(0), is("indexWithAllRoutingSettings")); ClusterState migratedState = ClusterState.builder(ClusterName.DEFAULT).metadata(mb).build(); - IndexMetadata migratedIndex = migratedState.metadata().index("indexWithRequireAndInclude"); - assertThat("index migration ONLY clears the setting it migrates, in this case the require.data setting", - migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), is("cold")); + IndexMetadata migratedIndex = migratedState.metadata().index("indexWithAllRoutingSettings"); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_INCLUDE_SETTING), nullValue()); assertThat(migratedIndex.getSettings().get(DATA_ROUTING_REQUIRE_SETTING), nullValue()); + assertThat(migratedIndex.getSettings().get(DATA_ROUTING_EXCLUDE_SETTING), nullValue()); assertThat(migratedIndex.getSettings().get(INDEX_ROUTING_PREFER), is("data_warm,data_hot")); } From 3d3473e2d989cf1477e8815cf342f2293730dc71 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 16 Jun 2021 19:19:48 +0100 Subject: [PATCH 16/19] Fix phase definition refresh for migrated policies where we remove the allocate action --- ...adataMigrateToDataTiersRoutingService.java | 195 ++++++++++++- ...MigrateToDataTiersRoutingServiceTests.java | 274 +++++++++++++++--- 2 files changed, 419 insertions(+), 50 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 3ad1ca0e5a00e..acd63f7eb5bdd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -23,10 +23,14 @@ import org.elasticsearch.xpack.core.ilm.AllocateAction; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +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.PhaseExecutionInfo; +import org.elasticsearch.xpack.core.ilm.Step; import java.time.Instant; import java.util.ArrayList; @@ -35,13 +39,19 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.SortedMap; +import java.util.Spliterators; import java.util.TreeMap; +import java.util.function.LongSupplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING; import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; +import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; @@ -133,7 +143,12 @@ public static Tuple migrateToDataTiersRouting(Cl attribute = DEFAULT_NODE_ATTRIBUTE_NAME; } List migratedPolicies = migrateIlmPolicies(mb, currentState, attribute, xContentRegistry, client); - List migratedIndices = migrateIndices(mb, currentState, attribute); + // Creating an intermediary cluster state view as when migrating policy we also update the cachesd phase definition stored in the + // index metadata so the metadata.builder will probably contain an already updated view over the indices metadata which we don't + // want to lose when migrating the indices settings + ClusterState intermediateState = ClusterState.builder(currentState).metadata(mb).build(); + mb = Metadata.builder(intermediateState.metadata()); + List migratedIndices = migrateIndices(mb, intermediateState, attribute); return Tuple.tuple(ClusterState.builder(currentState).metadata(mb).build(), new MigratedEntities(removedIndexTemplateName, migratedIndices, migratedPolicies)); } @@ -166,16 +181,188 @@ static List migrateIlmPolicies(Metadata.Builder mb, ClusterState current assert oldPolicyMetadata != null : "we must only update policies, not create new ones, but " + policyMetadataEntry.getKey() + " didn't exist"; - updateIndicesForPolicy(mb, currentState, xContentRegistry, client, oldPolicyMetadata.getPolicy(), newPolicyMetadata); + refreshCachedPhases(mb, currentState, oldPolicyMetadata, newPolicyMetadata, xContentRegistry, client); migratedPolicies.add(policyMetadataEntry.getKey()); } } - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); - mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); + if (migratedPolicies.size() > 0) { + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); + mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); + } return migratedPolicies; } + /** + * Refreshed the cached ILM phase definition for the indices managed by the migrated policy. + */ + static void refreshCachedPhases(Metadata.Builder mb, ClusterState currentState, LifecyclePolicyMetadata oldPolicyMetadata, + LifecyclePolicyMetadata newPolicyMetadata, NamedXContentRegistry xContentRegistry, + Client client) { + // this performs a walk through the managed indices and safely updates the cached phase (ie. for the phases we did not + // remove the allocate action) + updateIndicesForPolicy(mb, currentState, xContentRegistry, client, oldPolicyMetadata.getPolicy(), newPolicyMetadata); + + LifecyclePolicy newLifecyclePolicy = newPolicyMetadata.getPolicy(); + List migratedPhasesWithoutAllocateAction = + getMigratedPhasesWithoutAllocateAction(oldPolicyMetadata.getPolicy(), newLifecyclePolicy); + + if (migratedPhasesWithoutAllocateAction.size() > 0) { + logger.debug("the updated policy [{}] does not contain the allocate action in phases [{}] anymore", + newLifecyclePolicy.getName(), migratedPhasesWithoutAllocateAction); + // if we removed the allocate action in any phase we won't be able to perform a safe update of the ilm cached phase (as + // defined by {@link PhaseCacheManagement#isIndexPhaseDefinitionUpdatable} because the number of steps in the new phase is + // not the same as in the cached phase) so let's forcefully (and still safely :) ) refresh the cached phase for the managed + // indices in these phases. + refreshCachedPhaseForPhasesWithoutAllocateAction(mb, currentState, oldPolicyMetadata.getPolicy(), newPolicyMetadata, + migratedPhasesWithoutAllocateAction, client); + } + } + + /** + * Refresh the cached phase definition for those indices currently in one of the phases we migrated by removing the allocate action. + * This refresh can be executed in two ways, depending where exactly within such a migrated phase is currently the managed index. + * 1) if the index is in the allocate action, we'll move the ILM execution state for this index into the first step of the next + * action of the phase (note that even if the allocate action was the only action defined in a phase we have a complete action we + * inject at the end of every phase) + * 2) if the index is anywhere else in the phase, we simply update the cached phase definition to reflect the migrated phase + */ + private static void refreshCachedPhaseForPhasesWithoutAllocateAction(Metadata.Builder mb, ClusterState currentState, + LifecyclePolicy oldPolicy, + LifecyclePolicyMetadata newPolicyMetadata, + List phasesWithoutAllocateAction, Client client) { + String policyName = oldPolicy.getName(); + final List managedIndices = + StreamSupport.stream(Spliterators.spliteratorUnknownSize(currentState.metadata().indices().valuesIt(), 0), false) + .filter(meta -> policyName.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(meta.getSettings()))) + .collect(Collectors.toList()); + + for (IndexMetadata indexMetadata : managedIndices) { + LifecycleExecutionState currentExState = LifecycleExecutionState.fromIndexMetadata(indexMetadata); + + if (currentExState != null) { + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(currentExState); + if (currentStepKey != null && phasesWithoutAllocateAction.contains(currentStepKey.getPhase())) { + // the index is in a phase that doesn't contain the allocate action anymore + if (currentStepKey.getAction().equals(AllocateAction.NAME)) { + // this index is in the middle of executing the allocate action - which doesn't exist in the updated policy + // anymore so let's try to move the index to the next action + + LifecycleExecutionState newLifecycleState = moveStateToNextActionAndUpdateCachedPhase(indexMetadata, + currentExState, System::currentTimeMillis, oldPolicy, newPolicyMetadata, client); + if (currentExState.equals(newLifecycleState) == false) { + mb.put(IndexMetadata.builder(indexMetadata).putCustom(ILM_CUSTOM_METADATA_KEY, newLifecycleState.asMap())); + } + } else { + // if the index is not in the allocate action, we're going to perform a cached phase update (which is "unsafe" by + // the rules defined in {@link PhaseCacheManagement#isIndexPhaseDefinitionUpdatable} but in our case it is safe + // as the migration would've only removed the allocate action and the current index is not in the middle of + // executing the allocate action, we made sure of that) + + LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(currentExState); + PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(newPolicyMetadata.getPolicy().getName(), + newPolicyMetadata.getPolicy().getPhases().get(currentStepKey.getPhase()), newPolicyMetadata.getVersion(), + newPolicyMetadata.getModifiedDate()); + String newPhaseDefinition = Strings.toString(phaseExecutionInfo, false, false); + updatedState.setPhaseDefinition(newPhaseDefinition); + + logger.debug("updating the cached phase definition for index [{}], current step [{}] in policy " + + "[{}] to [{}]", indexMetadata.getIndex().getName(), currentStepKey, policyName, newPhaseDefinition); + mb.put(IndexMetadata.builder(indexMetadata) + .putCustom(ILM_CUSTOM_METADATA_KEY, updatedState.build().asMap())); + } + } + } + } + } + + /** + * Transition the managed index to the first step of the next action in the current phase and update the cached phase definition for + * the index to reflect the migrated phase definition. + * + * Returns the same {@link LifecycleExecutionState} if the transition is not possible or the new execution state otherwise. + */ + private static LifecycleExecutionState moveStateToNextActionAndUpdateCachedPhase(IndexMetadata indexMetadata, + LifecycleExecutionState existingState, + LongSupplier nowSupplier, LifecyclePolicy oldPolicy, + LifecyclePolicyMetadata newPolicyMetadata, + Client client) { + String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()); + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(existingState); + if (currentStepKey == null) { + logger.warn("unable to identify what the current step is for index [{}] as part of policy [{}]. the " + + "cached phase definition will not be updated for this index", indexMetadata.getIndex().getName(), policyName); + return existingState; + } + + List policySteps = oldPolicy.toSteps(client); + Optional currentStep = policySteps.stream() + .filter(step -> step.getKey().equals(currentStepKey)) + .findFirst(); + + if (currentStep.isPresent() == false) { + logger.warn("unable to find current step [{}] for index [{}] as part of policy [{}]. the cached phase definition will not be " + + "updated for this index", currentStepKey, indexMetadata.getIndex().getName(), policyName); + return existingState; + } + + int indexOfCurrentStep = policySteps.indexOf(currentStep.get()); + assert indexOfCurrentStep != -1 : "the current step must be part of the old policy"; + + Optional nextStepInActionAfterAllocate = policySteps.stream() + .skip(indexOfCurrentStep) + .filter(step -> step.getKey().getAction().equals(currentStepKey.getAction()) == false) + .findFirst(); + + assert nextStepInActionAfterAllocate.isPresent() : "there should always be a complete step at the end of every phase"; + Step.StepKey nextStep = nextStepInActionAfterAllocate.get().getKey(); + logger.debug("moving index [{}] in policy [{}] out of step [{}] to new step [{}]", + indexMetadata.getIndex().getName(), policyName, currentStepKey, nextStep); + + long nowAsMillis = nowSupplier.getAsLong(); + LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(existingState); + updatedState.setPhase(nextStep.getPhase()); + updatedState.setAction(nextStep.getAction()); + updatedState.setActionTime(nowAsMillis); + updatedState.setStep(nextStep.getName()); + updatedState.setStepTime(nowAsMillis); + updatedState.setFailedStep(null); + updatedState.setStepInfo(null); + updatedState.setIsAutoRetryableError(null); + updatedState.setFailedStepRetryCount(null); + + PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(newPolicyMetadata.getPolicy().getName(), + newPolicyMetadata.getPolicy().getPhases().get(currentStepKey.getPhase()), newPolicyMetadata.getVersion(), + newPolicyMetadata.getModifiedDate()); + updatedState.setPhaseDefinition(Strings.toString(phaseExecutionInfo, false, false)); + return updatedState.build(); + } + + /** + * Returns a list of phases that had an allocate action defined in the old policy, but don't have it anymore in the new policy + * (ie. they were allocate actions that only specified attribute based routing, without any number of replicas configuration and we + * removed them as part of the migration of ILM policies to data tiers in order to allow ILM to inject the migrate action) + */ + private static List getMigratedPhasesWithoutAllocateAction(LifecyclePolicy oldPolicy, LifecyclePolicy newLifecyclePolicy) { + List oldPhasesWithAllocateAction = new ArrayList<>(oldPolicy.getPhases().size()); + for (Map.Entry phaseEntry : oldPolicy.getPhases().entrySet()) { + if (phaseEntry.getValue().getActions().containsKey(AllocateAction.NAME)) { + oldPhasesWithAllocateAction.add(phaseEntry.getKey()); + } + } + + List migratedPhasesWithoutAllocateAction = new ArrayList<>(oldPhasesWithAllocateAction.size()); + for (String phaseWithAllocateAction : oldPhasesWithAllocateAction) { + Phase phase = newLifecyclePolicy.getPhases().get(phaseWithAllocateAction); + assert phase != null : "the migration service should not remove an entire phase altogether"; + if (phase.getActions().containsKey(AllocateAction.NAME) == false) { + // the updated policy doesn't have the allocate action defined in this phase anymore + migratedPhasesWithoutAllocateAction.add(phaseWithAllocateAction); + } + } + return migratedPhasesWithoutAllocateAction; + } + /** * Migrates a single ILM policy from defining {@link AllocateAction}s in order to configure shard allocation routing based on the * provided node attribute name towards allowing ILM to inject the {@link MigrateAction}. diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 97ea755fa8807..24a93beb8e8b1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -19,6 +19,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ParseField; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; @@ -32,9 +34,12 @@ import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.SetPriorityAction; import org.elasticsearch.xpack.core.ilm.ShrinkAction; import org.junit.Before; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -49,7 +54,6 @@ import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateToDataTiersRouting; import static org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider.INDEX_ROUTING_PREFER; import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -87,7 +91,9 @@ public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { ShrinkAction shrinkAction = new ShrinkAction(2, null); AllocateAction warmAllocateAction = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); AllocateAction coldAllocateAction = new AllocateAction(0, null, null, Map.of("data", "cold")); - LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(shrinkAction, warmAllocateAction, coldAllocateAction); + SetPriorityAction warmSetPriority = new SetPriorityAction(100); + LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(warmSetPriority, shrinkAction, warmAllocateAction, + coldAllocateAction); ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( @@ -105,8 +111,9 @@ public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { LifecyclePolicy lifecyclePolicy = updatedLifecycleMetadata.getPolicies().get(lifecycleName); Map warmActions = lifecyclePolicy.getPhases().get("warm").getActions(); assertThat("allocate action in the warm phase didn't specify any number of replicas so it must be removed", - warmActions.size(), is(1)); + warmActions.size(), is(2)); assertThat(warmActions.get(shrinkAction.getWriteableName()), is(shrinkAction)); + assertThat(warmActions.get(warmSetPriority.getWriteableName()), is(warmSetPriority)); Map coldActions = lifecyclePolicy.getPhases().get("cold").getActions(); assertThat(coldActions.size(), is(1)); @@ -115,55 +122,175 @@ public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { assertThat(migratedColdAllocateAction.getRequire().size(), is(0)); } + @SuppressWarnings("unchecked") public void testMigrateIlmPolicyRefreshesCachedPhase() { ShrinkAction shrinkAction = new ShrinkAction(2, null); AllocateAction warmAllocateAction = new AllocateAction(null, Map.of("data", "warm"), null, Map.of("rack", "rack1")); AllocateAction coldAllocateAction = new AllocateAction(0, null, null, Map.of("data", "cold")); - LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(shrinkAction, warmAllocateAction, coldAllocateAction); - - LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() - .setPhase("cold") - .setAction("allocate") - .setStep("allocate") - .setPhaseDefinition("{\n" + - " \"policy\" : \"" + lifecycleName + "\",\n" + - " \"phase_definition\" : {\n" + - " \"min_age\" : \"0m\",\n" + - " \"actions\" : {\n" + - " \"allocate\" : {\n" + - " \"number_of_replicas\" : \"0\",\n" + - " \"require\" : {\n" + - " \"data\": \"cold\"\n" + - " }\n" + - " }\n" + - " }\n" + - " },\n" + - " \"version\" : 1,\n" + - " \"modified_date_in_millis\" : 1578521007076\n" + - " }") - .build(); + SetPriorityAction warmSetPriority = new SetPriorityAction(100); + LifecyclePolicyMetadata policyMetadata = getWarmColdPolicyMeta(warmSetPriority, shrinkAction, warmAllocateAction, + coldAllocateAction); - IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) - .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); + { + // index is in the cold phase and the migrated allocate action is not removed + LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() + .setPhase("cold") + .setAction("allocate") + .setStep("allocate") + .setPhaseDefinition(getColdPhaseDefinition()) + .build(); - ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() - .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) - .put(indexMetadata).build()) - .build(); + IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) + .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); - Metadata.Builder newMetadata = Metadata.builder(state.metadata()); - List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) + .put(indexMetadata).build()) + .build(); - assertThat(migratedPolicies.get(0), is(lifecycleName)); - ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); - LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + + assertThat(migratedPolicies.get(0), is(lifecycleName)); + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + + Map migratedPhaseDefAsMap = getPhaseDefinitionAsMap(newLifecycleState); + + // expecting the phase definition to be refreshed with the migrated phase representation + // ie. allocate action does not contain any allocation rules + Map actions = (Map) migratedPhaseDefAsMap.get("actions"); + assertThat(actions.size(), is(1)); + Map allocateDef = (Map) actions.get(AllocateAction.NAME); + assertThat(allocateDef, notNullValue()); + assertThat(allocateDef.get("include"), is(Map.of())); + assertThat(allocateDef.get("exclude"), is(Map.of())); + assertThat(allocateDef.get("require"), is(Map.of())); + } + + { + // index is in the warm phase executing the allocate action, the migrated allocate action is removed + LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() + .setPhase("warm") + .setAction("allocate") + .setStep("allocate") + .setPhaseDefinition(getWarmPhaseDef()) + .build(); + + IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) + .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) + .put(indexMetadata).build()) + .build(); + + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + + assertThat(migratedPolicies.get(0), is(lifecycleName)); + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + + Map migratedPhaseDefAsMap = getPhaseDefinitionAsMap(newLifecycleState); + + // expecting the phase definition to be refreshed with the index being in the shrink action + Map actions = (Map) migratedPhaseDefAsMap.get("actions"); + assertThat(actions.size(), is(2)); + Map allocateDef = (Map) actions.get(AllocateAction.NAME); + assertThat(allocateDef, nullValue()); + assertThat(newLifecycleState.getAction(), is(ShrinkAction.NAME)); + assertThat(newLifecycleState.getStep(), is(ShrinkAction.CONDITIONAL_SKIP_SHRINK_STEP)); + + Map shrinkDef = (Map) actions.get(ShrinkAction.NAME); + assertThat(shrinkDef.get("number_of_shards"), is(2)); + Map setPriorityDef = (Map) actions.get(SetPriorityAction.NAME); + assertThat(setPriorityDef.get("priority"), is(100)); + } + + { + // index is in the warm phase executing the set priority action (executes BEFORE allocate), the migrated allocate action is + // removed + LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() + .setPhase("warm") + .setAction(SetPriorityAction.NAME) + .setStep(SetPriorityAction.NAME) + .setPhaseDefinition(getWarmPhaseDef()) + .build(); + + IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) + .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) + .put(indexMetadata).build()) + .build(); - // expecting the phase definition to be refreshed with the migrated phase representation - // ie. allocate action does not contain any allocation rules - String expectedRefreshedPhaseDefinition = "\"phase_definition\":{\"min_age\":\"0ms\"," + - "\"actions\":{\"allocate\":{\"number_of_replicas\":0,\"include\":{},\"exclude\":{},\"require\":{}}}}"; - assertThat(newLifecycleState.getPhaseDefinition(), containsString(expectedRefreshedPhaseDefinition)); + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + + assertThat(migratedPolicies.get(0), is(lifecycleName)); + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + Map migratedPhaseDefAsMap = getPhaseDefinitionAsMap(newLifecycleState); + + // expecting the phase definition to be refreshed with the index being in the set_priority action + Map actions = (Map) migratedPhaseDefAsMap.get("actions"); + assertThat(actions.size(), is(2)); + Map allocateDef = (Map) actions.get(AllocateAction.NAME); + assertThat(allocateDef, nullValue()); + Map shrinkDef = (Map) actions.get(ShrinkAction.NAME); + assertThat(shrinkDef.get("number_of_shards"), is(2)); + Map setPriorityDef = (Map) actions.get(SetPriorityAction.NAME); + assertThat(setPriorityDef.get("priority"), is(100)); + assertThat(newLifecycleState.getAction(), is(SetPriorityAction.NAME)); + assertThat(newLifecycleState.getStep(), is(SetPriorityAction.NAME)); + } + + { + // index is in the warm phase executing the shrink action (executes AFTER allocate), the migrated allocate action is + // removed + LifecycleExecutionState preMigrationExecutionState = LifecycleExecutionState.builder() + .setPhase("warm") + .setAction(ShrinkAction.NAME) + .setStep(ShrinkAction.CONDITIONAL_SKIP_SHRINK_STEP) + .setPhaseDefinition(getWarmPhaseDef()) + .build(); + + IndexMetadata.Builder indexMetadata = IndexMetadata.builder(indexName).settings(getBaseIndexSettings()) + .putCustom(ILM_CUSTOM_METADATA_KEY, preMigrationExecutionState.asMap()); + + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(Metadata.builder() + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( + Collections.singletonMap(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED)) + .put(indexMetadata).build()) + .build(); + + Metadata.Builder newMetadata = Metadata.builder(state.metadata()); + List migratedPolicies = migrateIlmPolicies(newMetadata, state, "data", REGISTRY, client); + + assertThat(migratedPolicies.get(0), is(lifecycleName)); + ClusterState newState = ClusterState.builder(state).metadata(newMetadata).build(); + LifecycleExecutionState newLifecycleState = LifecycleExecutionState.fromIndexMetadata(newState.metadata().index(indexName)); + + Map migratedPhaseDefAsMap = getPhaseDefinitionAsMap(newLifecycleState); + + // expecting the phase definition to be refreshed with the index being in the shrink action + Map actions = (Map) migratedPhaseDefAsMap.get("actions"); + assertThat(actions.size(), is(2)); + Map allocateDef = (Map) actions.get(AllocateAction.NAME); + assertThat(allocateDef, nullValue()); + assertThat(newLifecycleState.getAction(), is(ShrinkAction.NAME)); + assertThat(newLifecycleState.getStep(), is(ShrinkAction.CONDITIONAL_SKIP_SHRINK_STEP)); + + Map shrinkDef = (Map) actions.get(ShrinkAction.NAME); + assertThat(shrinkDef.get("number_of_shards"), is(2)); + Map setPriorityDef = (Map) actions.get(SetPriorityAction.NAME); + assertThat(setPriorityDef.get("priority"), is(100)); + } } private Settings.Builder getBaseIndexSettings() { @@ -523,16 +650,71 @@ public void testMigrationDoesNotRemoveComposableTemplates() { assertThat(migratedEntitiesTuple.v1().metadata().templatesV2().get(composableTemplateName), is(composableIndexTemplate)); } - private LifecyclePolicyMetadata getWarmColdPolicyMeta(ShrinkAction shrinkAction, AllocateAction warmAllocateAction, - AllocateAction coldAllocateAction) { + private LifecyclePolicyMetadata getWarmColdPolicyMeta(SetPriorityAction setPriorityAction, ShrinkAction shrinkAction, + AllocateAction warmAllocateAction, AllocateAction coldAllocateAction) { LifecyclePolicy policy = new LifecyclePolicy(lifecycleName, Map.of("warm", new Phase("warm", TimeValue.ZERO, Map.of(shrinkAction.getWriteableName(), shrinkAction, - warmAllocateAction.getWriteableName(), warmAllocateAction)), + warmAllocateAction.getWriteableName(), warmAllocateAction, setPriorityAction.getWriteableName(), setPriorityAction)), "cold", new Phase("cold", TimeValue.ZERO, Map.of(coldAllocateAction.getWriteableName(), coldAllocateAction)) )); return new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()); } + + private String getWarmPhaseDef() { + return "{\n" + + " \"policy\" : \"" + lifecycleName + "\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"0m\",\n" + + " \"actions\" : {\n" + + " \"allocate\" : {\n" + + " \"number_of_replicas\" : \"0\",\n" + + " \"require\" : {\n" + + " \"data\": \"cold\"\n" + + " }\n" + + " },\n" + + " \"set_priority\": {\n" + + " \"priority\": 100 \n" + + " },\n" + + " \"shrink\": {\n" + + " \"number_of_shards\": 2 \n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }"; + } + + private String getColdPhaseDefinition() { + return "{\n" + + " \"policy\" : \"" + lifecycleName + "\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"0m\",\n" + + " \"actions\" : {\n" + + " \"allocate\" : {\n" + + " \"number_of_replicas\" : \"0\",\n" + + " \"require\" : {\n" + + " \"data\": \"cold\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }"; + } + + @SuppressWarnings("unchecked") + private Map getPhaseDefinitionAsMap(LifecycleExecutionState newLifecycleState) { + XContentType entityContentType = XContentType.fromMediaType("application/json"); + Map migratedPhaseDefAsMap = (Map) XContentHelper.convertToMap(entityContentType.xContent(), + new ByteArrayInputStream(newLifecycleState.getPhaseDefinition().getBytes(StandardCharsets.UTF_8)), + false) + .get("phase_definition"); + return migratedPhaseDefAsMap; + } + } From 01ad3d9f4d62b7e85699106b60e3bf345ce2aa09 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 17 Jun 2021 16:05:31 +0100 Subject: [PATCH 17/19] Inline var --- .../MetadataMigrateToDataTiersRoutingServiceTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 24a93beb8e8b1..4d8b70c913dff 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -710,11 +710,10 @@ private String getColdPhaseDefinition() { @SuppressWarnings("unchecked") private Map getPhaseDefinitionAsMap(LifecycleExecutionState newLifecycleState) { XContentType entityContentType = XContentType.fromMediaType("application/json"); - Map migratedPhaseDefAsMap = (Map) XContentHelper.convertToMap(entityContentType.xContent(), + return (Map) XContentHelper.convertToMap(entityContentType.xContent(), new ByteArrayInputStream(newLifecycleState.getPhaseDefinition().getBytes(StandardCharsets.UTF_8)), false) .get("phase_definition"); - return migratedPhaseDefAsMap; } } From 354b645eddbda1d59679655a229c7b7bf9fd4679 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 17 Jun 2021 16:58:37 +0100 Subject: [PATCH 18/19] Move MetadataMigrateToDataTiersRoutingService to xpack.ilm --- .../metadata/MetadataMigrateToDataTiersRoutingService.java | 0 .../metadata/MetadataMigrateToDataTiersRoutingServiceTests.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename x-pack/plugin/{core => ilm}/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java (100%) rename x-pack/plugin/{core => ilm}/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java (100%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java similarity index 100% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java rename to x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java similarity index 100% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java rename to x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java From a5cd72ea1ad35d922438432f963dca593d07649b Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 17 Jun 2021 17:33:39 +0100 Subject: [PATCH 19/19] Extract moveStateToNextActionAndUpdateCachedPhase into IndexLifecycleTransition --- ...adataMigrateToDataTiersRoutingService.java | 66 +------------- .../xpack/ilm/IndexLifecycleTransition.java | 68 ++++++++++++++ .../ilm/IndexLifecycleTransitionTests.java | 90 ++++++++++++++++++- 3 files changed, 159 insertions(+), 65 deletions(-) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index acd63f7eb5bdd..e3288e74e6d98 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.cluster.metadata; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.client.Client; @@ -39,11 +40,9 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.SortedMap; import java.util.Spliterators; import java.util.TreeMap; -import java.util.function.LongSupplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -54,6 +53,7 @@ import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; +import static org.elasticsearch.xpack.ilm.IndexLifecycleTransition.moveStateToNextActionAndUpdateCachedPhase; /** * Exposes the necessary methods to migrate a system's elasticsearch abstractions to use data tiers for index allocation routing. @@ -276,68 +276,6 @@ private static void refreshCachedPhaseForPhasesWithoutAllocateAction(Metadata.Bu } } - /** - * Transition the managed index to the first step of the next action in the current phase and update the cached phase definition for - * the index to reflect the migrated phase definition. - * - * Returns the same {@link LifecycleExecutionState} if the transition is not possible or the new execution state otherwise. - */ - private static LifecycleExecutionState moveStateToNextActionAndUpdateCachedPhase(IndexMetadata indexMetadata, - LifecycleExecutionState existingState, - LongSupplier nowSupplier, LifecyclePolicy oldPolicy, - LifecyclePolicyMetadata newPolicyMetadata, - Client client) { - String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()); - Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(existingState); - if (currentStepKey == null) { - logger.warn("unable to identify what the current step is for index [{}] as part of policy [{}]. the " + - "cached phase definition will not be updated for this index", indexMetadata.getIndex().getName(), policyName); - return existingState; - } - - List policySteps = oldPolicy.toSteps(client); - Optional currentStep = policySteps.stream() - .filter(step -> step.getKey().equals(currentStepKey)) - .findFirst(); - - if (currentStep.isPresent() == false) { - logger.warn("unable to find current step [{}] for index [{}] as part of policy [{}]. the cached phase definition will not be " + - "updated for this index", currentStepKey, indexMetadata.getIndex().getName(), policyName); - return existingState; - } - - int indexOfCurrentStep = policySteps.indexOf(currentStep.get()); - assert indexOfCurrentStep != -1 : "the current step must be part of the old policy"; - - Optional nextStepInActionAfterAllocate = policySteps.stream() - .skip(indexOfCurrentStep) - .filter(step -> step.getKey().getAction().equals(currentStepKey.getAction()) == false) - .findFirst(); - - assert nextStepInActionAfterAllocate.isPresent() : "there should always be a complete step at the end of every phase"; - Step.StepKey nextStep = nextStepInActionAfterAllocate.get().getKey(); - logger.debug("moving index [{}] in policy [{}] out of step [{}] to new step [{}]", - indexMetadata.getIndex().getName(), policyName, currentStepKey, nextStep); - - long nowAsMillis = nowSupplier.getAsLong(); - LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(existingState); - updatedState.setPhase(nextStep.getPhase()); - updatedState.setAction(nextStep.getAction()); - updatedState.setActionTime(nowAsMillis); - updatedState.setStep(nextStep.getName()); - updatedState.setStepTime(nowAsMillis); - updatedState.setFailedStep(null); - updatedState.setStepInfo(null); - updatedState.setIsAutoRetryableError(null); - updatedState.setFailedStepRetryCount(null); - - PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(newPolicyMetadata.getPolicy().getName(), - newPolicyMetadata.getPolicy().getPhases().get(currentStepKey.getPhase()), newPolicyMetadata.getVersion(), - newPolicyMetadata.getModifiedDate()); - updatedState.setPhaseDefinition(Strings.toString(phaseExecutionInfo, false, false)); - return updatedState.build(); - } - /** * Returns a list of phases that had an allocate action defined in the old policy, but don't have it anymore in the new policy * (ie. they were allocate actions that only specified attribute based routing, without any number of replicas configuration and we diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java index e3aac468aa92a..be85b514b303c 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransition.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.TransportAction; +import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; @@ -27,6 +28,7 @@ import org.elasticsearch.xpack.core.ilm.InitializePolicyContextStep; import org.elasticsearch.xpack.core.ilm.InitializePolicyException; import org.elasticsearch.xpack.core.ilm.LifecycleExecutionState; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.Phase; @@ -39,6 +41,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.LongSupplier; @@ -261,6 +264,71 @@ private static LifecycleExecutionState updateExecutionStateToStep(LifecyclePolic return updatedState.build(); } + /** + * Transition the managed index to the first step of the next action in the current phase and update the cached phase definition for + * the index to reflect the new phase definition. + * + * The intended purpose of this method is to help with the situations where a policy is updated and we need to update the cached + * phase for the indices that are currently executing an action that was potentially removed in the new policy. + * + * Returns the same {@link LifecycleExecutionState} if the transition is not possible or the new execution state otherwise. + */ + public static LifecycleExecutionState moveStateToNextActionAndUpdateCachedPhase(IndexMetadata indexMetadata, + LifecycleExecutionState existingState, + LongSupplier nowSupplier, LifecyclePolicy oldPolicy, + LifecyclePolicyMetadata newPolicyMetadata, + Client client) { + String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()); + Step.StepKey currentStepKey = LifecycleExecutionState.getCurrentStepKey(existingState); + if (currentStepKey == null) { + logger.warn("unable to identify what the current step is for index [{}] as part of policy [{}]. the " + + "cached phase definition will not be updated for this index", indexMetadata.getIndex().getName(), policyName); + return existingState; + } + + List policySteps = oldPolicy.toSteps(client); + Optional currentStep = policySteps.stream() + .filter(step -> step.getKey().equals(currentStepKey)) + .findFirst(); + + if (currentStep.isPresent() == false) { + logger.warn("unable to find current step [{}] for index [{}] as part of policy [{}]. the cached phase definition will not be " + + "updated for this index", currentStepKey, indexMetadata.getIndex().getName(), policyName); + return existingState; + } + + int indexOfCurrentStep = policySteps.indexOf(currentStep.get()); + assert indexOfCurrentStep != -1 : "the current step must be part of the old policy"; + + Optional nextStepInActionAfterCurrent = policySteps.stream() + .skip(indexOfCurrentStep) + .filter(step -> step.getKey().getAction().equals(currentStepKey.getAction()) == false) + .findFirst(); + + assert nextStepInActionAfterCurrent.isPresent() : "there should always be a complete step at the end of every phase"; + Step.StepKey nextStep = nextStepInActionAfterCurrent.get().getKey(); + logger.debug("moving index [{}] in policy [{}] out of step [{}] to new step [{}]", + indexMetadata.getIndex().getName(), policyName, currentStepKey, nextStep); + + long nowAsMillis = nowSupplier.getAsLong(); + LifecycleExecutionState.Builder updatedState = LifecycleExecutionState.builder(existingState); + updatedState.setPhase(nextStep.getPhase()); + updatedState.setAction(nextStep.getAction()); + updatedState.setActionTime(nowAsMillis); + updatedState.setStep(nextStep.getName()); + updatedState.setStepTime(nowAsMillis); + updatedState.setFailedStep(null); + updatedState.setStepInfo(null); + updatedState.setIsAutoRetryableError(null); + updatedState.setFailedStepRetryCount(null); + + PhaseExecutionInfo phaseExecutionInfo = new PhaseExecutionInfo(newPolicyMetadata.getPolicy().getName(), + newPolicyMetadata.getPolicy().getPhases().get(currentStepKey.getPhase()), newPolicyMetadata.getVersion(), + newPolicyMetadata.getModifiedDate()); + updatedState.setPhaseDefinition(Strings.toString(phaseExecutionInfo, false, false)); + return updatedState.build(); + } + /** * Given a cluster state and lifecycle state, return a new state using the new lifecycle state for the given index. */ diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java index e0760e69075a4..00df2a9a4a022 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java @@ -9,19 +9,21 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; +import org.elasticsearch.client.Client; 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.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; import org.elasticsearch.xpack.core.ilm.AbstractStepTestCase; import org.elasticsearch.xpack.core.ilm.ErrorStep; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; @@ -35,6 +37,7 @@ import org.elasticsearch.xpack.core.ilm.MockStep; import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; import org.elasticsearch.xpack.core.ilm.RolloverAction; import org.elasticsearch.xpack.core.ilm.SetPriorityAction; import org.elasticsearch.xpack.core.ilm.Step; @@ -52,10 +55,12 @@ import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.eligibleToCheckForRefresh; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.refreshPhaseDefinition; import static org.elasticsearch.xpack.ilm.IndexLifecycleRunnerTests.createOneStepPolicyStepRegistry; +import static org.elasticsearch.xpack.ilm.IndexLifecycleTransition.moveStateToNextActionAndUpdateCachedPhase; import static org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils.newTestLifecyclePolicy; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; public class IndexLifecycleTransitionTests extends ESTestCase { @@ -781,6 +786,89 @@ public void testEligibleForRefresh() { assertTrue(eligibleToCheckForRefresh(meta)); } + public void testMoveStateToNextActionAndUpdateCachedPhase() { + LifecycleExecutionState.Builder currentExecutionState = LifecycleExecutionState.builder() + .setPhase("hot") + .setAction("rollover") + .setStep("check-rollover-ready") + .setPhaseDefinition("{\n" + + " \"policy\" : \"my-policy\",\n" + + " \"phase_definition\" : {\n" + + " \"min_age\" : \"20m\",\n" + + " \"actions\" : {\n" + + " \"rollover\" : {\n" + + " \"max_age\" : \"5s\"\n" + + " },\n" + + " \"set_priority\" : {\n" + + " \"priority\" : 150\n" + + " }\n" + + " }\n" + + " },\n" + + " \"version\" : 1,\n" + + " \"modified_date_in_millis\" : 1578521007076\n" + + " }"); + + IndexMetadata meta = buildIndexMetadata("my-policy", currentExecutionState); + + Map actions = new HashMap<>(); + actions.put("rollover", new RolloverAction(null, null, null, 1L)); + actions.put("set_priority", new SetPriorityAction(100)); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); + Map phases = Collections.singletonMap("hot", hotPhase); + LifecyclePolicy currentPolicy = new LifecyclePolicy("my-policy", phases); + + { + // test index is in step for action that was removed in the updated policy + // the expected new state is that the index was moved into the next *action* and its cached phase definition updated to reflect + // the updated policy + Map actionsWithoutRollover = new HashMap<>(); + actionsWithoutRollover.put("set_priority", new SetPriorityAction(100)); + Phase hotPhaseNoRollover = new Phase("hot", TimeValue.ZERO, actionsWithoutRollover); + Map phasesNoRollover = Collections.singletonMap("hot", hotPhaseNoRollover); + LifecyclePolicyMetadata updatedPolicyMetadata = new LifecyclePolicyMetadata(new LifecyclePolicy("my-policy", + phasesNoRollover), Collections.emptyMap(), 2L, 2L); + + try (Client client = new NoOpClient(getTestName())) { + LifecycleExecutionState newState = moveStateToNextActionAndUpdateCachedPhase(meta, + LifecycleExecutionState.fromIndexMetadata(meta), System::currentTimeMillis, currentPolicy, updatedPolicyMetadata, + client); + + Step.StepKey hotPhaseCompleteStepKey = PhaseCompleteStep.finalStep("hot").getKey(); + assertThat(newState.getAction(), is(hotPhaseCompleteStepKey.getAction())); + assertThat(newState.getStep(), is(hotPhaseCompleteStepKey.getName())); + assertThat("the cached phase should not contain rollover anymore", newState.getPhaseDefinition(), + not(containsString(RolloverAction.NAME))); + } + } + + { + // test that the index is in an action that still exists in the update policy + // the expected new state is that the index is moved into the next action (could be the complete one) and the cached phase + // definition is updated + Map actionsWitoutSetPriority = new HashMap<>(); + actionsWitoutSetPriority.put("rollover", new RolloverAction(null, null, null, 1L)); + Phase hotPhaseNoSetPriority = new Phase("hot", TimeValue.ZERO, actionsWitoutSetPriority); + Map phasesWithoutSetPriority = Collections.singletonMap("hot", hotPhaseNoSetPriority); + LifecyclePolicyMetadata updatedPolicyMetadata = new LifecyclePolicyMetadata(new LifecyclePolicy("my-policy", + phasesWithoutSetPriority), Collections.emptyMap(), 2L, 2L); + + try (Client client = new NoOpClient(getTestName())) { + LifecycleExecutionState newState = moveStateToNextActionAndUpdateCachedPhase(meta, + LifecycleExecutionState.fromIndexMetadata(meta), System::currentTimeMillis, currentPolicy, updatedPolicyMetadata, + client); + + Step.StepKey hotPhaseCompleteStepKey = PhaseCompleteStep.finalStep("hot").getKey(); + // the state was still moved into the next action, even if the updated policy still contained the action the index was + // currently executing + assertThat(newState.getAction(), is(hotPhaseCompleteStepKey.getAction())); + assertThat(newState.getStep(), is(hotPhaseCompleteStepKey.getName())); + assertThat(newState.getPhaseDefinition(), containsString(RolloverAction.NAME)); + assertThat("the cached phase should not contain set_priority anymore", newState.getPhaseDefinition(), + not(containsString(SetPriorityAction.NAME))); + } + } + } + private static LifecyclePolicy createPolicy(String policyName, Step.StepKey safeStep, Step.StepKey unsafeStep) { Map phases = new HashMap<>(); if (safeStep != null) {