Skip to content

Commit 7d45355

Browse files
authored
ILM: Add support for the searchable_snapshot action in the hot phase (#64883)
This adds support for the searchable_snapshot ILM action in the hot phase. We define a series of actions that cannot be executed after the index has been mounted as a searchable snapshot. Namely: freeze, forcemerge, shrink, and searchable_snapshot (also available in the cold phase). If by virtue of snapshot/restoring a managed index or updating an ILM policy while it is executing for an index, these actions could get to be executed on an index that was mounted as searchable snapshot in the hot phase. If this happens the actions will skip entirely. ILM will not move into the ERROR step.
1 parent b873e1d commit 7d45355

File tree

16 files changed

+452
-130
lines changed

16 files changed

+452
-130
lines changed

docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44

55
beta::[]
66

7-
Phases allowed: cold.
7+
Phases allowed: hot, cold.
88

99
Takes a snapshot of the managed index in the configured repository
1010
and mounts it as a searchable snapshot.
1111
If the managed index is part of a <<data-streams, data stream>>,
1212
the mounted index replaces the original index in the data stream.
1313

14+
To use the `searchable_snapshot` action in the `hot` phase, the `rollover`
15+
action *must* be present. If no rollover action is configured, {ilm-init}
16+
will reject the policy.
17+
18+
IMPORTANT: If the `searchable_snapshot` action is used in the `hot` phase the
19+
subsequent phases cannot define any of the `shrink`, `forcemerge`, `freeze` or
20+
`searchable_snapshot` (also available in the cold phase) actions.
21+
1422
[NOTE]
1523
This action cannot be performed on a data stream's write index. Attempts to do
1624
so will fail. To convert the index to a searchable snapshot, first

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncRetryDuringSnapshotActionStep.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* registers an observer and waits to try again when a snapshot is no longer running.
2727
*/
2828
public abstract class AsyncRetryDuringSnapshotActionStep extends AsyncActionStep {
29-
private final Logger logger = LogManager.getLogger(AsyncRetryDuringSnapshotActionStep.class);
29+
private static final Logger logger = LogManager.getLogger(AsyncRetryDuringSnapshotActionStep.class);
3030

3131
public AsyncRetryDuringSnapshotActionStep(StepKey key, StepKey nextStepKey, Client client) {
3232
super(key, nextStepKey, client);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeAction.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package org.elasticsearch.xpack.core.ilm;
77

8+
import org.apache.logging.log4j.LogManager;
9+
import org.apache.logging.log4j.Logger;
810
import org.elasticsearch.Version;
911
import org.elasticsearch.client.Client;
1012
import org.elasticsearch.cluster.health.ClusterHealthStatus;
@@ -31,9 +33,12 @@
3133
* A {@link LifecycleAction} which force-merges the index.
3234
*/
3335
public class ForceMergeAction implements LifecycleAction {
36+
private static final Logger logger = LogManager.getLogger(ForceMergeAction.class);
37+
3438
public static final String NAME = "forcemerge";
3539
public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments");
3640
public static final ParseField CODEC = new ParseField("index_codec");
41+
public static final String CONDITIONAL_SKIP_FORCE_MERGE_STEP = BranchingStep.NAME + "-forcemerge-check-prerequisites";
3742

3843
private static final ConstructingObjectParser<ForceMergeAction, Void> PARSER = new ConstructingObjectParser<>(NAME,
3944
false, a -> {
@@ -120,6 +125,7 @@ public List<Step> toSteps(Client client, String phase, Step.StepKey nextStepKey)
120125

121126
final boolean codecChange = codec != null && codec.equals(CodecService.BEST_COMPRESSION_CODEC);
122127

128+
StepKey preForceMergeBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_FORCE_MERGE_STEP);
123129
StepKey checkNotWriteIndex = new StepKey(phase, NAME, CheckNotDataStreamWriteIndexStep.NAME);
124130
StepKey readOnlyKey = new StepKey(phase, NAME, ReadOnlyAction.NAME);
125131

@@ -131,6 +137,18 @@ public List<Step> toSteps(Client client, String phase, Step.StepKey nextStepKey)
131137
StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME);
132138
StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME);
133139

140+
BranchingStep conditionalSkipShrinkStep = new BranchingStep(preForceMergeBranchingKey, checkNotWriteIndex, nextStepKey,
141+
(index, clusterState) -> {
142+
IndexMetadata indexMetadata = clusterState.metadata().index(index);
143+
assert indexMetadata != null : "index " + index.getName() + " must exist in the cluster state";
144+
if (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) {
145+
String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings());
146+
logger.warn("[{}] action is configured for index [{}] in policy [{}] which is mounted as searchable snapshot. " +
147+
"Skipping this action", ForceMergeAction.NAME, index.getName(), policyName);
148+
return true;
149+
}
150+
return false;
151+
});
134152
CheckNotDataStreamWriteIndexStep checkNotWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNotWriteIndex,
135153
readOnlyKey);
136154
UpdateSettingsStep readOnlyStep =
@@ -147,6 +165,7 @@ public List<Step> toSteps(Client client, String phase, Step.StepKey nextStepKey)
147165
SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments);
148166

149167
List<Step> mergeSteps = new ArrayList<>();
168+
mergeSteps.add(conditionalSkipShrinkStep);
150169
mergeSteps.add(checkNotWriteIndexStep);
151170
mergeSteps.add(readOnlyStep);
152171

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/FreezeAction.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
*/
66
package org.elasticsearch.xpack.core.ilm;
77

8+
import org.apache.logging.log4j.LogManager;
9+
import org.apache.logging.log4j.Logger;
810
import org.elasticsearch.client.Client;
11+
import org.elasticsearch.cluster.metadata.IndexMetadata;
912
import org.elasticsearch.common.Strings;
1013
import org.elasticsearch.common.io.stream.StreamInput;
1114
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -22,7 +25,10 @@
2225
* A {@link LifecycleAction} which freezes the index.
2326
*/
2427
public class FreezeAction implements LifecycleAction {
28+
private static final Logger logger = LogManager.getLogger(FreezeAction.class);
29+
2530
public static final String NAME = "freeze";
31+
public static final String CONDITIONAL_SKIP_FREEZE_STEP = BranchingStep.NAME + "-freeze-check-prerequisites";
2632

2733
private static final ObjectParser<FreezeAction, Void> PARSER = new ObjectParser<>(NAME, FreezeAction::new);
2834

@@ -59,13 +65,31 @@ public boolean isSafeAction() {
5965

6066
@Override
6167
public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
68+
StepKey preFreezeMergeBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_FREEZE_STEP);
6269
StepKey checkNotWriteIndex = new StepKey(phase, NAME, CheckNotDataStreamWriteIndexStep.NAME);
6370
StepKey freezeStepKey = new StepKey(phase, NAME, FreezeStep.NAME);
6471

72+
BranchingStep conditionalSkipFreezeStep = new BranchingStep(preFreezeMergeBranchingKey, checkNotWriteIndex, nextStepKey,
73+
(index, clusterState) -> {
74+
IndexMetadata indexMetadata = clusterState.getMetadata().index(index);
75+
assert indexMetadata != null : "index " + index.getName() + " must exist in the cluster state";
76+
String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings());
77+
if (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) {
78+
logger.warn("[{}] action is configured for index [{}] in policy [{}] which is mounted as searchable snapshot. " +
79+
"Skipping this action", FreezeAction.NAME, index.getName(), policyName);
80+
return true;
81+
}
82+
if (indexMetadata.getSettings().getAsBoolean("index.frozen", false)) {
83+
logger.debug("skipping [{}] action for index [{}] in policy [{}] as the index is already frozen", FreezeAction.NAME,
84+
index.getName(), policyName);
85+
return true;
86+
}
87+
return false;
88+
});
6589
CheckNotDataStreamWriteIndexStep checkNoWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNotWriteIndex,
6690
freezeStepKey);
6791
FreezeStep freezeStep = new FreezeStep(freezeStepKey, nextStepKey, client);
68-
return Arrays.asList(checkNoWriteIndexStep, freezeStep);
92+
return Arrays.asList(conditionalSkipFreezeStep, checkNoWriteIndexStep, freezeStep);
6993
}
7094

7195
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleSettings.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public class LifecycleSettings {
2727
public static final String SLM_RETENTION_DURATION = "slm.retention_duration";
2828
public static final String SLM_MINIMUM_INTERVAL = "slm.minimum_interval";
2929

30+
// This is not a setting configuring ILM per se, but certain ILM actions need to validate the managed index is not
31+
// already mounted as a searchable snapshot. Those ILM actions will check if the index has this setting name configured.
32+
public static final String SNAPSHOT_INDEX_NAME = "index.store.snapshot.index_name";
33+
3034
public static final Setting<TimeValue> LIFECYCLE_POLL_INTERVAL_SETTING = Setting.timeSetting(LIFECYCLE_POLL_INTERVAL,
3135
TimeValue.timeValueMinutes(10), TimeValue.timeValueSeconds(1), Setting.Property.Dynamic, Setting.Property.NodeScope);
3236
public static final Setting<String> LIFECYCLE_NAME_SETTING = Setting.simpleString(LIFECYCLE_NAME,

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
*/
66
package org.elasticsearch.xpack.core.ilm;
77

8+
import org.apache.logging.log4j.LogManager;
9+
import org.apache.logging.log4j.Logger;
810
import org.elasticsearch.Version;
911
import org.elasticsearch.client.Client;
1012
import org.elasticsearch.cluster.health.ClusterHealthStatus;
1113
import org.elasticsearch.cluster.metadata.IndexAbstraction;
14+
import org.elasticsearch.cluster.metadata.IndexMetadata;
1215
import org.elasticsearch.common.ParseField;
1316
import org.elasticsearch.common.Strings;
1417
import org.elasticsearch.common.io.stream.StreamInput;
@@ -29,11 +32,14 @@
2932
* newly created searchable snapshot backed index.
3033
*/
3134
public class SearchableSnapshotAction implements LifecycleAction {
35+
private static final Logger logger = LogManager.getLogger(SearchableSnapshotAction.class);
36+
3237
public static final String NAME = "searchable_snapshot";
3338

3439
public static final ParseField SNAPSHOT_REPOSITORY = new ParseField("snapshot_repository");
3540
public static final ParseField FORCE_MERGE_INDEX = new ParseField("force_merge_index");
3641
public static final String CONDITIONAL_DATASTREAM_CHECK_KEY = BranchingStep.NAME + "-on-datastream-check";
42+
public static final String CONDITIONAL_SKIP_ACTION_STEP = BranchingStep.NAME + "-check-prerequisites";
3743

3844
public static final String RESTORED_INDEX_PREFIX = "restored-";
3945

@@ -74,6 +80,7 @@ boolean isForceMergeIndex() {
7480

7581
@Override
7682
public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
83+
StepKey preActionBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_ACTION_STEP);
7784
StepKey checkNoWriteIndex = new StepKey(phase, NAME, CheckNotDataStreamWriteIndexStep.NAME);
7885
StepKey waitForNoFollowerStepKey = new StepKey(phase, NAME, WaitForNoFollowersStep.NAME);
7986
StepKey forceMergeStepKey = new StepKey(phase, NAME, ForceMergeStep.NAME);
@@ -90,6 +97,18 @@ public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
9097
StepKey replaceDataStreamIndexKey = new StepKey(phase, NAME, ReplaceDataStreamBackingIndexStep.NAME);
9198
StepKey deleteIndexKey = new StepKey(phase, NAME, DeleteStep.NAME);
9299

100+
BranchingStep conditionalSkipActionStep = new BranchingStep(preActionBranchingKey, checkNoWriteIndex, nextStepKey,
101+
(index, clusterState) -> {
102+
IndexMetadata indexMetadata = clusterState.getMetadata().index(index);
103+
assert indexMetadata != null : "index " + index.getName() + " must exist in the cluster state";
104+
if (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) {
105+
logger.warn("[{}] action is configured for index [{}] in policy [{}] which is already mounted as searchable " +
106+
"snapshot. Skipping this action", SearchableSnapshotAction.NAME, index.getName(),
107+
LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()));
108+
return true;
109+
}
110+
return false;
111+
});
93112
CheckNotDataStreamWriteIndexStep checkNoWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNoWriteIndex,
94113
waitForNoFollowerStepKey);
95114
final WaitForNoFollowersStep waitForNoFollowersStep;
@@ -130,6 +149,7 @@ public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
130149
null, client, RESTORED_INDEX_PREFIX);
131150

132151
List<Step> steps = new ArrayList<>();
152+
steps.add(conditionalSkipActionStep);
133153
steps.add(checkNoWriteIndexStep);
134154
steps.add(waitForNoFollowersStep);
135155
if (forceMergeIndex) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
* A {@link LifecycleAction} which shrinks the index.
3030
*/
3131
public class ShrinkAction implements LifecycleAction {
32+
private static final Logger logger = LogManager.getLogger(ShrinkAction.class);
33+
3234
public static final String NAME = "shrink";
3335
public static final String SHRUNKEN_INDEX_PREFIX = "shrink-";
3436
public static final ParseField NUMBER_OF_SHARDS_FIELD = new ParseField("number_of_shards");
3537
public static final String CONDITIONAL_SKIP_SHRINK_STEP = BranchingStep.NAME + "-check-prerequisites";
3638
public static final String CONDITIONAL_DATASTREAM_CHECK_KEY = BranchingStep.NAME + "-on-datastream-check";
3739

38-
private static final Logger logger = LogManager.getLogger(ShrinkAction.class);
39-
4040
private static final ConstructingObjectParser<ShrinkAction, Void> PARSER =
4141
new ConstructingObjectParser<>(NAME, a -> new ShrinkAction((Integer) a[0]));
4242

@@ -108,7 +108,19 @@ public List<Step> toSteps(Client client, String phase, Step.StepKey nextStepKey)
108108
StepKey deleteIndexKey = new StepKey(phase, NAME, DeleteStep.NAME);
109109

110110
BranchingStep conditionalSkipShrinkStep = new BranchingStep(preShrinkBranchingKey, checkNotWriteIndex, nextStepKey,
111-
(index, clusterState) -> clusterState.getMetadata().index(index).getNumberOfShards() == numberOfShards);
111+
(index, clusterState) -> {
112+
IndexMetadata indexMetadata = clusterState.getMetadata().index(index);
113+
if (indexMetadata.getNumberOfShards() == numberOfShards) {
114+
return true;
115+
}
116+
if (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) {
117+
logger.warn("[{}] action is configured for index [{}] in policy [{}] which is mounted as searchable snapshot. " +
118+
"Skipping this action", ShrinkAction.NAME, indexMetadata.getIndex().getName(),
119+
LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings()));
120+
return true;
121+
}
122+
return false;
123+
});
112124
CheckNotDataStreamWriteIndexStep checkNotWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNotWriteIndex,
113125
waitForNoFollowerStepKey);
114126
WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, readOnlyKey, client);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.ArrayList;
1414
import java.util.Arrays;
1515
import java.util.Collection;
16+
import java.util.Collections;
1617
import java.util.HashMap;
1718
import java.util.List;
1819
import java.util.Map;
@@ -40,7 +41,7 @@ public class TimeseriesLifecycleType implements LifecycleType {
4041
static final String DELETE_PHASE = "delete";
4142
static final List<String> VALID_PHASES = Arrays.asList(HOT_PHASE, WARM_PHASE, COLD_PHASE, DELETE_PHASE);
4243
static final List<String> ORDERED_VALID_HOT_ACTIONS = Arrays.asList(SetPriorityAction.NAME, UnfollowAction.NAME, RolloverAction.NAME,
43-
ReadOnlyAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME);
44+
ReadOnlyAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME, SearchableSnapshotAction.NAME);
4445
static final List<String> ORDERED_VALID_WARM_ACTIONS = Arrays.asList(SetPriorityAction.NAME, UnfollowAction.NAME, ReadOnlyAction.NAME,
4546
AllocateAction.NAME, MigrateAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME);
4647
static final List<String> ORDERED_VALID_COLD_ACTIONS = Arrays.asList(SetPriorityAction.NAME, UnfollowAction.NAME, AllocateAction.NAME,
@@ -57,7 +58,10 @@ public class TimeseriesLifecycleType implements LifecycleType {
5758
DELETE_PHASE, VALID_DELETE_ACTIONS);
5859

5960
static final Set<String> HOT_ACTIONS_THAT_REQUIRE_ROLLOVER = Sets.newHashSet(ReadOnlyAction.NAME, ShrinkAction.NAME,
60-
ForceMergeAction.NAME);
61+
ForceMergeAction.NAME, SearchableSnapshotAction.NAME);
62+
// a set of actions that cannot be defined (executed) after the managed index has been mounted as searchable snapshot
63+
static final Set<String> ACTIONS_CANNOT_FOLLOW_SEARCHABLE_SNAPSHOT = Sets.newHashSet(ShrinkAction.NAME, ForceMergeAction.NAME,
64+
FreezeAction.NAME, SearchableSnapshotAction.NAME);
6165

6266
private TimeseriesLifecycleType() {
6367
}
@@ -255,6 +259,29 @@ public void validate(Collection<Phase> phases) {
255259
MigrateAction.NAME + " action and an " + AllocateAction.NAME + " action with allocation rules. specify only a single " +
256260
"data migration in each phase");
257261
}
262+
263+
validateActionsFollowingSearchableSnapshot(phases);
264+
}
265+
266+
static void validateActionsFollowingSearchableSnapshot(Collection<Phase> phases) {
267+
boolean hotPhaseContainsSearchableSnapshot = phases.stream()
268+
.filter(phase -> HOT_PHASE.equals(phase.getName()))
269+
.anyMatch(phase -> phase.getActions().containsKey(SearchableSnapshotAction.NAME));
270+
if (hotPhaseContainsSearchableSnapshot) {
271+
String phasesDefiningIllegalActions = phases.stream()
272+
// we're looking for prohibited actions in phases other than hot
273+
.filter(phase -> HOT_PHASE.equals(phase.getName()) == false)
274+
// filter the phases that define illegal actions
275+
.filter(phase ->
276+
Collections.disjoint(ACTIONS_CANNOT_FOLLOW_SEARCHABLE_SNAPSHOT, phase.getActions().keySet()) == false)
277+
.map(Phase::getName)
278+
.collect(Collectors.joining(","));
279+
if (Strings.hasText(phasesDefiningIllegalActions)) {
280+
throw new IllegalArgumentException("phases [" + phasesDefiningIllegalActions + "] define one or more of " +
281+
ACTIONS_CANNOT_FOLLOW_SEARCHABLE_SNAPSHOT + " actions which are not allowed after a " +
282+
"managed index is mounted as a searchable snapshot");
283+
}
284+
}
258285
}
259286

260287
private static boolean definesAllocationRules(AllocateAction action) {

0 commit comments

Comments
 (0)