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 a31eb4d8274ac..7325c1c356090 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 @@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -21,15 +20,13 @@ import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; import org.elasticsearch.xpack.core.DataTier; import org.elasticsearch.xpack.core.ilm.Step.StepKey; +import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.FROZEN_PHASE; - /** * A {@link LifecycleAction} which enables or disables the automatic migration of data between * {@link org.elasticsearch.xpack.core.DataTier}s. @@ -103,40 +100,34 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { StepKey migrationKey = new StepKey(phase, NAME, NAME); StepKey migrationRoutedKey = new StepKey(phase, NAME, DataTierMigrationRoutedStep.NAME); - Settings.Builder migrationSettings = Settings.builder(); String targetTier = "data_" + phase; assert DataTier.validTierName(targetTier) : "invalid data tier name:" + targetTier; BranchingStep conditionalSkipActionStep = new BranchingStep(preMigrateBranchingKey, migrationKey, nextStepKey, (index, clusterState) -> { - if (skipMigrateAction(phase, clusterState.metadata().index(index))) { - String policyName = - LifecycleSettings.LIFECYCLE_NAME_SETTING.get(clusterState.metadata().index(index).getSettings()); - logger.debug("[{}] action is configured for index [{}] in policy [{}] which is already mounted as a searchable " + - "snapshot. skipping this action", MigrateAction.NAME, index.getName(), policyName); + Settings indexSettings = clusterState.metadata().index(index).getSettings(); + + // partially mounted indices will already have data_frozen, and we don't want to change that if they do + if (SearchableSnapshotsConstants.isPartialSearchableSnapshotIndex(indexSettings)) { + String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexSettings); + logger.debug("[{}] action in policy [{}] is configured for index [{}] which is a partially mounted index. " + + "skipping this action", MigrateAction.NAME, policyName, index.getName()); return true; } - // don't skip the migrate action as the index is not mounted as searchable snapshot or we're in the frozen phase return false; }); - migrationSettings.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, getPreferredTiersConfiguration(targetTier)); UpdateSettingsStep updateMigrationSettingStep = new UpdateSettingsStep(migrationKey, migrationRoutedKey, client, - migrationSettings.build()); + Settings.builder() + .put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, getPreferredTiersConfiguration(targetTier)) + .build()); DataTierMigrationRoutedStep migrationRoutedStep = new DataTierMigrationRoutedStep(migrationRoutedKey, nextStepKey); - return Arrays.asList(conditionalSkipActionStep, updateMigrationSettingStep, migrationRoutedStep); + return List.of(conditionalSkipActionStep, updateMigrationSettingStep, migrationRoutedStep); } else { return List.of(); } } - static boolean skipMigrateAction(String phase, IndexMetadata indexMetadata) { - // if the index is a searchable snapshot we skip the migrate action (as mounting an index as searchable snapshot - // configures the tier allocation preference), unless we're in the frozen phase - return (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) - && (phase.equals(FROZEN_PHASE) == false); - } - /** * 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` diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java index 5ad38d1ac33e0..5ae746a77f082 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java @@ -17,6 +17,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; +import org.elasticsearch.xpack.core.DataTier; import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction; import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest; @@ -101,10 +103,14 @@ void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentCl indexName = snapshotIndexName; } + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), Boolean.FALSE.toString()); + // if we are mounting a searchable snapshot in the hot phase, then the index should be pinned to the hot nodes + if (TimeseriesLifecycleType.HOT_PHASE.equals(this.getKey().getPhase())) { + settingsBuilder.put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_HOT); + } final MountSearchableSnapshotRequest mountSearchableSnapshotRequest = new MountSearchableSnapshotRequest(mountedIndexName, - snapshotRepository, snapshotName, indexName, Settings.builder() - .put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), Boolean.FALSE.toString()) - .build(), + snapshotRepository, snapshotName, indexName, settingsBuilder.build(), // we captured the index metadata when we took the snapshot. the index likely had the ILM execution state in the metadata. // if we were to restore the lifecycle.name setting, the restored index would be captured by the ILM runner and, // depending on what ILM execution state was captured at snapshot time, make it's way forward from _that_ step forward in 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 8149b39ec29cf..dd6c35a64c9dc 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 @@ -7,30 +7,29 @@ package org.elasticsearch.xpack.core.ilm; import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.Writeable.Reader; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; import java.util.List; -import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; -import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; -import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_VERSION_CREATED; +import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; 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.MigrateAction.skipMigrateAction; 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.FROZEN_PHASE; import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.HOT_PHASE; import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.WARM_PHASE; +import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants.SNAPSHOT_DIRECTORY_FACTORY_KEY; +import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants.SNAPSHOT_PARTIAL_SETTING; import static org.hamcrest.CoreMatchers.is; public class MigrateActionTests extends AbstractActionTestCase { @@ -116,43 +115,50 @@ public void testMigrateActionsConfiguresTierPreference() { } } - public void testSkipMigrateAction() { - IndexMetadata snappedIndex = IndexMetadata.builder("snapped_index") - .settings( - Settings.builder() - .put(LifecycleSettings.SNAPSHOT_INDEX_NAME, "snapped") - .put(SETTING_VERSION_CREATED, Version.CURRENT) - .put(SETTING_NUMBER_OF_SHARDS, 1) - .put(SETTING_NUMBER_OF_REPLICAS, 0) - ) - .build(); - - IndexMetadata regularIndex = IndexMetadata.builder("regular_index") - .settings( - Settings.builder() - .put(SETTING_VERSION_CREATED, Version.CURRENT) - .put(SETTING_NUMBER_OF_SHARDS, 1) - .put(SETTING_NUMBER_OF_REPLICAS, 0) - ) - .build(); + public void testMigrateActionWillSkipAPartiallyMountedIndex() { + StepKey nextStepKey = new StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10), + randomAlphaOfLengthBetween(1, 10)); + MigrateAction action = new MigrateAction(); + // does not skip an ordinary index { - // migrate action is not skipped if the index is not a searchable snapshot - Arrays.asList(HOT_PHASE, WARM_PHASE, COLD_PHASE, FROZEN_PHASE) - .forEach(phase -> assertThat(skipMigrateAction(phase, regularIndex), is(false))); - } + IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); - { - // migrate action is skipped if the index is a searchable snapshot for phases hot -> cold - Arrays.asList(HOT_PHASE, WARM_PHASE, COLD_PHASE) - .forEach(phase -> assertThat(skipMigrateAction(phase, snappedIndex), is(true))); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + List steps = action.toSteps(null, HOT_PHASE, nextStepKey); + BranchingStep firstStep = (BranchingStep) steps.get(0); + UpdateSettingsStep secondStep = (UpdateSettingsStep) steps.get(1); + firstStep.performAction(indexMetadata.getIndex(), clusterState); + + assertEquals(secondStep.getKey(), firstStep.getNextStepKey()); } + // does skip a partially mounted { - // migrate action is never skipped for the frozen phase - assertThat(skipMigrateAction(FROZEN_PHASE, snappedIndex), is(false)); - assertThat(skipMigrateAction(FROZEN_PHASE, regularIndex), is(false)); + IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(5)) + .settings(settings(Version.CURRENT) + .put(INDEX_STORE_TYPE_SETTING.getKey(), SNAPSHOT_DIRECTORY_FACTORY_KEY) + .put(SNAPSHOT_PARTIAL_SETTING.getKey(), true)) + .numberOfShards(1) + .numberOfReplicas(2) + .build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")) + .metadata(Metadata.builder().put(indexMetadata, true).build()) + .build(); + + List steps = action.toSteps(null, HOT_PHASE, nextStepKey); + BranchingStep firstStep = (BranchingStep) steps.get(0); + firstStep.performAction(indexMetadata.getIndex(), clusterState); + + assertEquals(nextStepKey, firstStep.getNextStepKey()); } } - } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java index abc50cd065618..a809b08ce903e 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java @@ -28,7 +28,6 @@ import org.elasticsearch.xpack.core.ilm.LifecycleAction; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.LifecycleSettings; -import org.elasticsearch.xpack.core.ilm.MigrateAction; import org.elasticsearch.xpack.core.ilm.Phase; import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; import org.elasticsearch.xpack.core.ilm.RolloverAction; @@ -40,7 +39,6 @@ import java.io.IOException; import java.io.InputStream; -import java.time.ZonedDateTime; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -161,8 +159,8 @@ public void testSearchableSnapshotForceMergesIndexToOneSegment() throws Exceptio TimeUnit.SECONDS); } - @SuppressWarnings("unchecked") - public void testDeleteActionDeletesSearchableSnapshot() throws Exception { + @SuppressWarnings("unchecked") + public void testDeleteActionDeletesSearchableSnapshot() throws Exception { createSnapshotRepo(client(), snapshotRepo, randomBoolean()); // create policy with cold and delete phases @@ -198,21 +196,21 @@ public void testDeleteActionDeletesSearchableSnapshot() throws Exception { assertBusy(() -> assertFalse(indexExists(restoredIndexName)), 60, TimeUnit.SECONDS); assertTrue("the snapshot we generate in the cold phase should be deleted by the delete phase", waitUntil(() -> { - try { - Request getSnapshotsRequest = new Request("GET", "_snapshot/" + snapshotRepo + "/_all"); - Response getSnapshotsResponse = client().performRequest(getSnapshotsRequest); - - Map responseMap; - try (InputStream is = getSnapshotsResponse.getEntity().getContent()) { - responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); - } - List responses = (List) responseMap.get("responses"); - Object snapshots = ((Map) responses.get(0)).get("snapshots"); - return ((List>) snapshots).size() == 0; - } catch (Exception e) { - logger.error(e.getMessage(), e); - return false; - } + try { + Request getSnapshotsRequest = new Request("GET", "_snapshot/" + snapshotRepo + "/_all"); + Response getSnapshotsResponse = client().performRequest(getSnapshotsRequest); + + Map responseMap; + try (InputStream is = getSnapshotsResponse.getEntity().getContent()) { + responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + List responses = (List) responseMap.get("responses"); + Object snapshots = ((Map) responses.get(0)).get("snapshots"); + return ((List>) snapshots).size() == 0; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return false; + } }, 30, TimeUnit.SECONDS)); } @@ -478,19 +476,14 @@ public void testSecondSearchableSnapshotUsingDifferentRepoThrows() throws Except containsString("policy specifies [searchable_snapshot] action multiple times with differing repositories")); } - public void testSearchableSnapshotActionOverridesMigrateAction() throws Exception { + public void testSearchableSnapshotsInHotPhasePinnedToHotNodes() throws Exception { createSnapshotRepo(client(), snapshotRepo, randomBoolean()); createPolicy(client(), policy, new Phase("hot", TimeValue.ZERO, Map.of(RolloverAction.NAME, new RolloverAction(null, null, null, 1L), SearchableSnapshotAction.NAME, new SearchableSnapshotAction( snapshotRepo, randomBoolean())) ), - new Phase("warm", TimeValue.ZERO, Map.of(MigrateAction.NAME, new MigrateAction(true))), - // this time transition condition will make sure we catch ILM in the warm phase so we can assert the warm migrate action - // didn't re-configure the tier allocation settings set by the searchable_snapshot action in the hot phase - // we'll use the origination date to kick off ILM to complete the policy - new Phase("cold", TimeValue.timeValueDays(5L), Map.of(MigrateAction.NAME, new MigrateAction(true))), - null, null + null, null, null, null ); createComposableTemplate(client(), randomAlphaOfLengthBetween(5, 10).toLowerCase(), dataStream, @@ -513,25 +506,12 @@ snapshotRepo, randomBoolean())) logger.info("--> waiting for [{}] to exist...", restoredIndex); assertTrue(indexExists(restoredIndex)); }, 30, TimeUnit.SECONDS); - assertBusy(() -> assertThat(getStepKeyForIndex(client(), restoredIndex), is(PhaseCompleteStep.finalStep("warm").getKey())), + assertBusy(() -> assertThat(getStepKeyForIndex(client(), restoredIndex), is(PhaseCompleteStep.finalStep("hot").getKey())), 30, TimeUnit.SECONDS); - Map warmIndexSettings = getIndexSettingsAsMap(restoredIndex); - // the warm phase shouldn't have changed the data_cold -> data_hot configuration - assertThat(warmIndexSettings.get(DataTierAllocationDecider.INDEX_ROUTING_PREFER), - is("data_cold,data_warm,data_hot")); - - // make the index 100 days old so the cold phase transition timing passes - updateIndexSettings(restoredIndex, Settings.builder().put(LifecycleSettings.LIFECYCLE_ORIGINATION_DATE, - ZonedDateTime.now().toInstant().toEpochMilli() - TimeValue.timeValueDays(100).getMillis())); - - // let's wait for ILM to finish - assertBusy(() -> assertThat(getStepKeyForIndex(client(), restoredIndex), is(PhaseCompleteStep.finalStep("cold").getKey()))); - - Map coldIndexSettings = getIndexSettingsAsMap(restoredIndex); - // the frozen phase should've reconfigured the allocation preference - assertThat(coldIndexSettings.get(DataTierAllocationDecider.INDEX_ROUTING_PREFER), - is("data_cold,data_warm,data_hot")); + Map hotIndexSettings = getIndexSettingsAsMap(restoredIndex); + // searchable snapshots mounted in the hot phase should be pinned to hot nodes + assertThat(hotIndexSettings.get(DataTierAllocationDecider.INDEX_ROUTING_PREFER), + is("data_hot")); } - }