Skip to content

Commit 55c0d1a

Browse files
authored
TSDB: Implement Downsampling ILM Action for time-series indices (#87269)
This PR adds support for an ILM action that downsamples a time-series index by invoking the _rollup endpoint (#85708) A policy that includes the rollup action will look like the following PUT _ilm/policy/my_policy { "policy": { "phases": { "warm": { "actions": { "rollup": { "fixed_interval": "24h" } } } } } } Relates to #74660 Fixes #68609
1 parent ce9f94b commit 55c0d1a

23 files changed

+1155
-230
lines changed

docs/changelog/87269.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 87269
2+
summary: "TSDB: Implement downsampling ILM Action for time-series indices"
3+
area: TSDB
4+
type: feature
5+
issues:
6+
- 68609

server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,7 @@ public Index getResizeSourceIndex() {
11381138
);
11391139

11401140
public enum RollupTaskStatus {
1141+
UNKNOWN,
11411142
STARTED,
11421143
SUCCESS;
11431144

@@ -1150,7 +1151,7 @@ public String toString() {
11501151
public static final Setting<RollupTaskStatus> INDEX_ROLLUP_STATUS = Setting.enumSetting(
11511152
RollupTaskStatus.class,
11521153
INDEX_ROLLUP_STATUS_KEY,
1153-
RollupTaskStatus.SUCCESS,
1154+
RollupTaskStatus.UNKNOWN,
11541155
Property.IndexScope,
11551156
Property.InternalIndex
11561157
);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.core.ilm;
8+
9+
import org.apache.logging.log4j.LogManager;
10+
import org.apache.logging.log4j.Logger;
11+
import org.elasticsearch.action.ActionListener;
12+
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
13+
import org.elasticsearch.action.support.master.AcknowledgedResponse;
14+
import org.elasticsearch.client.internal.Client;
15+
import org.elasticsearch.cluster.ClusterState;
16+
import org.elasticsearch.cluster.metadata.IndexMetadata;
17+
import org.elasticsearch.common.Strings;
18+
import org.elasticsearch.core.TimeValue;
19+
import org.elasticsearch.index.IndexNotFoundException;
20+
21+
import java.util.Objects;
22+
import java.util.function.Function;
23+
24+
/**
25+
* Deletes the target index created by an operation such as shrink or rollup and
26+
* identified the target index name stored in the lifecycle state of the managed
27+
* index (if any was generated)
28+
*/
29+
public class CleanupTargetIndexStep extends AsyncRetryDuringSnapshotActionStep {
30+
public static final String NAME = "cleanup-target-index";
31+
private static final Logger logger = LogManager.getLogger(CleanupTargetIndexStep.class);
32+
33+
private final Function<IndexMetadata, String> sourceIndexNameSupplier;
34+
private final Function<IndexMetadata, String> targetIndexNameSupplier;
35+
36+
public CleanupTargetIndexStep(
37+
StepKey key,
38+
StepKey nextStepKey,
39+
Client client,
40+
Function<IndexMetadata, String> sourceIndexNameSupplier,
41+
Function<IndexMetadata, String> targetIndexNameSupplier
42+
) {
43+
super(key, nextStepKey, client);
44+
this.sourceIndexNameSupplier = sourceIndexNameSupplier;
45+
this.targetIndexNameSupplier = targetIndexNameSupplier;
46+
}
47+
48+
@Override
49+
public boolean isRetryable() {
50+
return true;
51+
}
52+
53+
Function<IndexMetadata, String> getSourceIndexNameSupplier() {
54+
return sourceIndexNameSupplier;
55+
}
56+
57+
Function<IndexMetadata, String> getTargetIndexNameSupplier() {
58+
return targetIndexNameSupplier;
59+
}
60+
61+
@Override
62+
void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentClusterState, ActionListener<Void> listener) {
63+
final String sourceIndexName = sourceIndexNameSupplier.apply(indexMetadata);
64+
if (Strings.isNullOrEmpty(sourceIndexName) == false) {
65+
// the current managed index is the target index
66+
if (currentClusterState.metadata().index(sourceIndexName) == null) {
67+
// if the source index does not exist, we'll skip deleting the
68+
// (managed) target index as that will cause data loss
69+
String policyName = indexMetadata.getLifecyclePolicyName();
70+
logger.warn(
71+
"managed index [{}] has been created as part of policy [{}] and the source index [{}] does not exist "
72+
+ "anymore. will skip the [{}] step",
73+
indexMetadata.getIndex().getName(),
74+
policyName,
75+
sourceIndexName,
76+
NAME
77+
);
78+
listener.onResponse(null);
79+
return;
80+
}
81+
}
82+
83+
final String targetIndexName = targetIndexNameSupplier.apply(indexMetadata);
84+
// if the target index was not generated there is nothing to delete so we move on
85+
if (Strings.hasText(targetIndexName) == false) {
86+
listener.onResponse(null);
87+
return;
88+
}
89+
getClient().admin()
90+
.indices()
91+
.delete(new DeleteIndexRequest(targetIndexName).masterNodeTimeout(TimeValue.MAX_VALUE), new ActionListener<>() {
92+
@Override
93+
public void onResponse(AcknowledgedResponse acknowledgedResponse) {
94+
// even if not all nodes acked the delete request yet we can consider this operation as successful as
95+
// we'll generate a new index name and attempt to create an index with the newly generated name
96+
listener.onResponse(null);
97+
}
98+
99+
@Override
100+
public void onFailure(Exception e) {
101+
if (e instanceof IndexNotFoundException) {
102+
// we can move on if the index was deleted in the meantime
103+
listener.onResponse(null);
104+
} else {
105+
listener.onFailure(e);
106+
}
107+
}
108+
});
109+
}
110+
111+
@Override
112+
public boolean equals(Object o) {
113+
if (this == o) {
114+
return true;
115+
}
116+
if (o == null || getClass() != o.getClass()) {
117+
return false;
118+
}
119+
CleanupTargetIndexStep that = (CleanupTargetIndexStep) o;
120+
return super.equals(o)
121+
&& Objects.equals(targetIndexNameSupplier, that.targetIndexNameSupplier)
122+
&& Objects.equals(sourceIndexNameSupplier, that.sourceIndexNameSupplier);
123+
}
124+
125+
@Override
126+
public int hashCode() {
127+
return Objects.hash(super.hashCode(), targetIndexNameSupplier, sourceIndexNameSupplier);
128+
}
129+
}

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,41 @@
1010
import org.apache.logging.log4j.Logger;
1111
import org.elasticsearch.cluster.ClusterState;
1212
import org.elasticsearch.cluster.metadata.IndexMetadata;
13+
import org.elasticsearch.cluster.metadata.LifecycleExecutionState;
1314
import org.elasticsearch.cluster.metadata.Metadata;
1415
import org.elasticsearch.common.settings.Settings;
1516
import org.elasticsearch.index.Index;
1617

1718
import java.util.Locale;
1819
import java.util.Objects;
20+
import java.util.function.BiFunction;
1921

2022
/**
2123
* Copy the provided settings from the source to the target index.
2224
* <p>
23-
* The target index is derived from the source index using the provided prefix.
24-
* This is useful for actions like shrink or searchable snapshot that create a new index and migrate the ILM execution from the source
25-
* to the target index.
25+
* The target index is generated by a supplier function.
26+
* This is useful for actions like shrink, rollup or searchable snapshot that create
27+
* a new index and migrate the ILM execution from the source to the target index.
2628
*/
2729
public class CopySettingsStep extends ClusterStateActionStep {
2830
public static final String NAME = "copy-settings";
2931

3032
private static final Logger logger = LogManager.getLogger(CopySettingsStep.class);
3133

3234
private final String[] settingsKeys;
33-
private final String indexPrefix;
3435

35-
public CopySettingsStep(StepKey key, StepKey nextStepKey, String indexPrefix, String... settingsKeys) {
36+
private final BiFunction<String, LifecycleExecutionState, String> targetIndexNameSupplier;
37+
38+
public CopySettingsStep(
39+
StepKey key,
40+
StepKey nextStepKey,
41+
BiFunction<String, LifecycleExecutionState, String> targetIndexNameSupplier,
42+
String... settingsKeys
43+
) {
3644
super(key, nextStepKey);
37-
Objects.requireNonNull(indexPrefix);
3845
Objects.requireNonNull(settingsKeys);
39-
this.indexPrefix = indexPrefix;
4046
this.settingsKeys = settingsKeys;
47+
this.targetIndexNameSupplier = targetIndexNameSupplier;
4148
}
4249

4350
@Override
@@ -49,17 +56,14 @@ public String[] getSettingsKeys() {
4956
return settingsKeys;
5057
}
5158

52-
public String getIndexPrefix() {
53-
return indexPrefix;
54-
}
59+
BiFunction<String, LifecycleExecutionState, String> getTargetIndexNameSupplier() {
60+
return targetIndexNameSupplier;
61+
};
5562

5663
@Override
5764
public ClusterState performAction(Index index, ClusterState clusterState) {
5865
String sourceIndexName = index.getName();
5966
IndexMetadata sourceIndexMetadata = clusterState.metadata().index(sourceIndexName);
60-
String targetIndexName = indexPrefix + sourceIndexName;
61-
IndexMetadata targetIndexMetadata = clusterState.metadata().index(targetIndexName);
62-
6367
if (sourceIndexMetadata == null) {
6468
// Index must have been since deleted, ignore it
6569
logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), sourceIndexName);
@@ -70,6 +74,8 @@ public ClusterState performAction(Index index, ClusterState clusterState) {
7074
return clusterState;
7175
}
7276

77+
String targetIndexName = targetIndexNameSupplier.apply(sourceIndexName, sourceIndexMetadata.getLifecycleExecutionState());
78+
IndexMetadata targetIndexMetadata = clusterState.metadata().index(targetIndexName);
7379
if (targetIndexMetadata == null) {
7480
String errorMessage = String.format(
7581
Locale.ROOT,
@@ -107,11 +113,13 @@ public boolean equals(Object o) {
107113
return false;
108114
}
109115
CopySettingsStep that = (CopySettingsStep) o;
110-
return Objects.equals(settingsKeys, that.settingsKeys) && Objects.equals(indexPrefix, that.indexPrefix);
116+
return super.equals(o)
117+
&& Objects.equals(targetIndexNameSupplier, that.targetIndexNameSupplier)
118+
&& Objects.equals(settingsKeys, that.settingsKeys);
111119
}
112120

113121
@Override
114122
public int hashCode() {
115-
return Objects.hash(super.hashCode(), settingsKeys, indexPrefix);
123+
return Objects.hash(super.hashCode(), targetIndexNameSupplier, settingsKeys);
116124
}
117125
}

0 commit comments

Comments
 (0)