Skip to content

Commit 5b2c317

Browse files
samxbrhappysubinelasticsearchmachine
authored
[8.19] [ILM]: Fix TSDS unfollow timing with WaitUntilTimeSeriesEndTimePassesStep (#128361) (#129518)
* [ILM]: Fix TSDS unfollow timing with WaitUntilTimeSeriesEndTimePassesStep (#128361) The backing indices of a time series data streams (TSDS) have time ranges (start_time & end_time) and they include documents that belong to these time ranges. To ensure that we will not unfollow a leader TSDS index before the indexing is complete, we need to add a WaitUntilTimeSeriesEndTimePassesStep to the unfollow action. This will ensure that we will only unfollow after the end_time has passed. This creates some weird semantics with the combination of the rollover and the unfollow. Because we need the rollover of the leader index to finalise the end_time but the unfollow action is injected before the rollover. However, this should be fine, because the leader index will skip the unfollow action so it will rollover and finalise the end_time and the follower index will wait the end_time to pass before it unfollows. Rolling over the follower index will have no effect since it’s already rolled over. (cherry picked from commit ed7f2ca) # Conflicts: # x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/UnfollowAction.java * Fix missing method * [CI] Auto commit changes from spotless --------- Co-authored-by: 안수빈 <[email protected]> Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 68d3761 commit 5b2c317

File tree

5 files changed

+192
-25
lines changed

5 files changed

+192
-25
lines changed

docs/changelog/128361.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 128361
2+
summary: The follower index should wait until the time series end time passes before unfollowing the leader index.
3+
area: ILM+SLM
4+
type: bug
5+
issues:
6+
- 128129

test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,9 +2116,16 @@ protected static boolean aliasExists(String index, String alias) throws IOExcept
21162116
/**
21172117
* Returns a list of the data stream's backing index names.
21182118
*/
2119-
@SuppressWarnings("unchecked")
21202119
protected static List<String> getDataStreamBackingIndexNames(String dataStreamName) throws IOException {
2121-
Map<String, Object> response = getAsMap(client(), "/_data_stream/" + dataStreamName);
2120+
return getDataStreamBackingIndexNames(client(), dataStreamName);
2121+
}
2122+
2123+
/**
2124+
* Returns a list of the data stream's backing index names.
2125+
*/
2126+
@SuppressWarnings("unchecked")
2127+
protected static List<String> getDataStreamBackingIndexNames(RestClient client, String dataStreamName) throws IOException {
2128+
Map<String, Object> response = getAsMap(client, "/_data_stream/" + dataStreamName);
21222129
List<?> dataStreams = (List<?>) response.get("data_streams");
21232130
assertThat(dataStreams.size(), equalTo(1));
21242131
Map<?, ?> dataStream = (Map<?, ?>) dataStreams.get(0);

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.xpack.core.ilm.Step.StepKey;
1818

1919
import java.io.IOException;
20+
import java.time.Instant;
2021
import java.util.List;
2122
import java.util.Map;
2223

@@ -44,6 +45,7 @@ private UnfollowAction() {}
4445
public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
4546
StepKey preUnfollowKey = new StepKey(phase, NAME, CONDITIONAL_UNFOLLOW_STEP);
4647
StepKey indexingComplete = new StepKey(phase, NAME, WaitForIndexingCompleteStep.NAME);
48+
StepKey waitUntilTimeSeriesEndTimePassesStep = new StepKey(phase, NAME, WaitUntilTimeSeriesEndTimePassesStep.NAME);
4749
StepKey waitForFollowShardTasks = new StepKey(phase, NAME, WaitForFollowShardTasksStep.NAME);
4850
StepKey pauseFollowerIndex = new StepKey(phase, NAME, PauseFollowerIndexStep.NAME);
4951
StepKey closeFollowerIndex = new StepKey(phase, NAME, CloseFollowerIndexStep.NAME);
@@ -64,14 +66,19 @@ public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
6466
return customIndexMetadata == null;
6567
}
6668
);
67-
WaitForIndexingCompleteStep step1 = new WaitForIndexingCompleteStep(indexingComplete, waitForFollowShardTasks);
68-
WaitForFollowShardTasksStep step2 = new WaitForFollowShardTasksStep(waitForFollowShardTasks, pauseFollowerIndex, client);
69-
PauseFollowerIndexStep step3 = new PauseFollowerIndexStep(pauseFollowerIndex, closeFollowerIndex, client);
70-
CloseFollowerIndexStep step4 = new CloseFollowerIndexStep(closeFollowerIndex, unfollowFollowerIndex, client);
71-
UnfollowFollowerIndexStep step5 = new UnfollowFollowerIndexStep(unfollowFollowerIndex, openFollowerIndex, client);
72-
OpenIndexStep step6 = new OpenIndexStep(openFollowerIndex, waitForYellowStep, client);
73-
WaitForIndexColorStep step7 = new WaitForIndexColorStep(waitForYellowStep, nextStepKey, ClusterHealthStatus.YELLOW);
74-
return List.of(conditionalSkipUnfollowStep, step1, step2, step3, step4, step5, step6, step7);
69+
WaitForIndexingCompleteStep step1 = new WaitForIndexingCompleteStep(indexingComplete, waitUntilTimeSeriesEndTimePassesStep);
70+
WaitUntilTimeSeriesEndTimePassesStep step2 = new WaitUntilTimeSeriesEndTimePassesStep(
71+
waitUntilTimeSeriesEndTimePassesStep,
72+
waitForFollowShardTasks,
73+
Instant::now
74+
);
75+
WaitForFollowShardTasksStep step3 = new WaitForFollowShardTasksStep(waitForFollowShardTasks, pauseFollowerIndex, client);
76+
PauseFollowerIndexStep step4 = new PauseFollowerIndexStep(pauseFollowerIndex, closeFollowerIndex, client);
77+
CloseFollowerIndexStep step5 = new CloseFollowerIndexStep(closeFollowerIndex, unfollowFollowerIndex, client);
78+
UnfollowFollowerIndexStep step6 = new UnfollowFollowerIndexStep(unfollowFollowerIndex, openFollowerIndex, client);
79+
OpenIndexStep step7 = new OpenIndexStep(openFollowerIndex, waitForYellowStep, client);
80+
WaitForIndexColorStep step8 = new WaitForIndexColorStep(waitForYellowStep, nextStepKey, ClusterHealthStatus.YELLOW);
81+
return List.of(conditionalSkipUnfollowStep, step1, step2, step3, step4, step5, step6, step7, step8);
7582
}
7683

7784
@Override

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UnfollowActionTests.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ public void testToSteps() {
5151
);
5252
List<Step> steps = action.toSteps(null, phase, nextStepKey);
5353
assertThat(steps, notNullValue());
54-
assertThat(steps.size(), equalTo(8));
54+
assertThat(steps.size(), equalTo(9));
5555

5656
StepKey expectedFirstStepKey = new StepKey(phase, UnfollowAction.NAME, UnfollowAction.CONDITIONAL_UNFOLLOW_STEP);
5757
StepKey expectedSecondStepKey = new StepKey(phase, UnfollowAction.NAME, WaitForIndexingCompleteStep.NAME);
58-
StepKey expectedThirdStepKey = new StepKey(phase, UnfollowAction.NAME, WaitForFollowShardTasksStep.NAME);
59-
StepKey expectedFourthStepKey = new StepKey(phase, UnfollowAction.NAME, PauseFollowerIndexStep.NAME);
60-
StepKey expectedFifthStepKey = new StepKey(phase, UnfollowAction.NAME, CloseFollowerIndexStep.NAME);
61-
StepKey expectedSixthStepKey = new StepKey(phase, UnfollowAction.NAME, UnfollowFollowerIndexStep.NAME);
62-
StepKey expectedSeventhStepKey = new StepKey(phase, UnfollowAction.NAME, OPEN_FOLLOWER_INDEX_STEP_NAME);
63-
StepKey expectedEighthStepKey = new StepKey(phase, UnfollowAction.NAME, WaitForIndexColorStep.NAME);
58+
StepKey expectedThirdStepKey = new StepKey(phase, UnfollowAction.NAME, WaitUntilTimeSeriesEndTimePassesStep.NAME);
59+
StepKey expectedFourthStepKey = new StepKey(phase, UnfollowAction.NAME, WaitForFollowShardTasksStep.NAME);
60+
StepKey expectedFifthStepKey = new StepKey(phase, UnfollowAction.NAME, PauseFollowerIndexStep.NAME);
61+
StepKey expectedSixthStepKey = new StepKey(phase, UnfollowAction.NAME, CloseFollowerIndexStep.NAME);
62+
StepKey expectedSeventhStepKey = new StepKey(phase, UnfollowAction.NAME, UnfollowFollowerIndexStep.NAME);
63+
StepKey expectedEighthStepKey = new StepKey(phase, UnfollowAction.NAME, OPEN_FOLLOWER_INDEX_STEP_NAME);
64+
StepKey expectedNinthStepKey = new StepKey(phase, UnfollowAction.NAME, WaitForIndexColorStep.NAME);
6465

6566
BranchingStep firstStep = (BranchingStep) steps.get(0);
6667
assertThat(firstStep.getKey(), equalTo(expectedFirstStepKey));
@@ -69,30 +70,34 @@ public void testToSteps() {
6970
assertThat(secondStep.getKey(), equalTo(expectedSecondStepKey));
7071
assertThat(secondStep.getNextStepKey(), equalTo(expectedThirdStepKey));
7172

72-
WaitForFollowShardTasksStep thirdStep = (WaitForFollowShardTasksStep) steps.get(2);
73+
WaitUntilTimeSeriesEndTimePassesStep thirdStep = (WaitUntilTimeSeriesEndTimePassesStep) steps.get(2);
7374
assertThat(thirdStep.getKey(), equalTo(expectedThirdStepKey));
7475
assertThat(thirdStep.getNextStepKey(), equalTo(expectedFourthStepKey));
7576

76-
PauseFollowerIndexStep fourthStep = (PauseFollowerIndexStep) steps.get(3);
77+
WaitForFollowShardTasksStep fourthStep = (WaitForFollowShardTasksStep) steps.get(3);
7778
assertThat(fourthStep.getKey(), equalTo(expectedFourthStepKey));
7879
assertThat(fourthStep.getNextStepKey(), equalTo(expectedFifthStepKey));
7980

80-
CloseFollowerIndexStep fifthStep = (CloseFollowerIndexStep) steps.get(4);
81+
PauseFollowerIndexStep fifthStep = (PauseFollowerIndexStep) steps.get(4);
8182
assertThat(fifthStep.getKey(), equalTo(expectedFifthStepKey));
8283
assertThat(fifthStep.getNextStepKey(), equalTo(expectedSixthStepKey));
8384

84-
UnfollowFollowerIndexStep sixthStep = (UnfollowFollowerIndexStep) steps.get(5);
85+
CloseFollowerIndexStep sixthStep = (CloseFollowerIndexStep) steps.get(5);
8586
assertThat(sixthStep.getKey(), equalTo(expectedSixthStepKey));
8687
assertThat(sixthStep.getNextStepKey(), equalTo(expectedSeventhStepKey));
8788

88-
OpenIndexStep seventhStep = (OpenIndexStep) steps.get(6);
89+
UnfollowFollowerIndexStep seventhStep = (UnfollowFollowerIndexStep) steps.get(6);
8990
assertThat(seventhStep.getKey(), equalTo(expectedSeventhStepKey));
9091
assertThat(seventhStep.getNextStepKey(), equalTo(expectedEighthStepKey));
9192

92-
WaitForIndexColorStep eighthStep = (WaitForIndexColorStep) steps.get(7);
93-
assertThat(eighthStep.getColor(), is(ClusterHealthStatus.YELLOW));
93+
OpenIndexStep eighthStep = (OpenIndexStep) steps.get(7);
9494
assertThat(eighthStep.getKey(), equalTo(expectedEighthStepKey));
95-
assertThat(eighthStep.getNextStepKey(), equalTo(nextStepKey));
95+
assertThat(eighthStep.getNextStepKey(), equalTo(expectedNinthStepKey));
96+
97+
WaitForIndexColorStep ninth = (WaitForIndexColorStep) steps.get(8);
98+
assertThat(ninth.getColor(), is(ClusterHealthStatus.YELLOW));
99+
assertThat(ninth.getKey(), equalTo(expectedNinthStepKey));
100+
assertThat(ninth.getNextStepKey(), equalTo(nextStepKey));
96101
}
97102

98103
@Override

x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import org.elasticsearch.client.RestClient;
1818
import org.elasticsearch.common.Strings;
1919
import org.elasticsearch.common.settings.Settings;
20+
import org.elasticsearch.common.time.DateFormatter;
21+
import org.elasticsearch.common.time.FormatNames;
2022
import org.elasticsearch.common.xcontent.XContentHelper;
2123
import org.elasticsearch.core.TimeValue;
2224
import org.elasticsearch.rest.RestStatus;
@@ -28,9 +30,11 @@
2830
import org.elasticsearch.xpack.core.ilm.LifecyclePolicy;
2931
import org.elasticsearch.xpack.core.ilm.Phase;
3032
import org.elasticsearch.xpack.core.ilm.UnfollowAction;
33+
import org.elasticsearch.xpack.core.ilm.WaitUntilTimeSeriesEndTimePassesStep;
3134

3235
import java.io.IOException;
3336
import java.io.InputStream;
37+
import java.time.Instant;
3438
import java.util.List;
3539
import java.util.Locale;
3640
import java.util.Map;
@@ -39,14 +43,47 @@
3943

4044
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
4145
import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX;
46+
import static org.hamcrest.Matchers.containsString;
4247
import static org.hamcrest.Matchers.equalTo;
48+
import static org.hamcrest.Matchers.greaterThan;
4349
import static org.hamcrest.Matchers.is;
4450
import static org.hamcrest.Matchers.notNullValue;
4551
import static org.hamcrest.Matchers.nullValue;
4652

4753
public class CCRIndexLifecycleIT extends ESCCRRestTestCase {
4854

4955
private static final Logger LOGGER = LogManager.getLogger(CCRIndexLifecycleIT.class);
56+
private static final String TSDB_INDEX_TEMPLATE = """
57+
{
58+
"index_patterns": ["%s*"],
59+
"data_stream": {},
60+
"template": {
61+
"settings":{
62+
"index": {
63+
"number_of_replicas": 0,
64+
"number_of_shards": 1,
65+
"routing_path": ["metricset"],
66+
"mode": "time_series"
67+
},
68+
"index.lifecycle.name": "%s"
69+
},
70+
"mappings":{
71+
"properties": {
72+
"@timestamp" : {
73+
"type": "date"
74+
},
75+
"metricset": {
76+
"type": "keyword",
77+
"time_series_dimension": true
78+
},
79+
"volume": {
80+
"type": "double",
81+
"time_series_metric": "gauge"
82+
}
83+
}
84+
}
85+
}
86+
}""";
5087

5188
public void testBasicCCRAndILMIntegration() throws Exception {
5289
String indexName = "logs-1";
@@ -533,6 +570,91 @@ public void testILMUnfollowFailsToRemoveRetentionLeases() throws Exception {
533570
}
534571
}
535572

573+
@SuppressWarnings("unchecked")
574+
public void testTsdbLeaderIndexRolloverAndSyncAfterWaitUntilEndTime() throws Exception {
575+
String indexPattern = "tsdb-index-";
576+
String dataStream = "tsdb-index-cpu";
577+
String policyName = "tsdb-policy";
578+
579+
if ("leader".equals(targetCluster)) {
580+
putILMPolicy(policyName, null, 1, null);
581+
Request templateRequest = new Request("PUT", "/_index_template/tsdb_template");
582+
templateRequest.setJsonEntity(Strings.format(TSDB_INDEX_TEMPLATE, indexPattern, policyName));
583+
assertOK(client().performRequest(templateRequest));
584+
} else if ("follow".equals(targetCluster)) {
585+
// Use unfollow-only policy for follower cluster instead of regular ILM policy
586+
// Follower clusters should not have their own rollover actions as they are meant
587+
// to follow the rollover behavior of the leader index, not initiate their own rollovers
588+
putUnfollowOnlyPolicy(client(), policyName);
589+
590+
Request createAutoFollowRequest = new Request("PUT", "/_ccr/auto_follow/tsdb_index_auto_follow_pattern");
591+
createAutoFollowRequest.setJsonEntity("""
592+
{
593+
"leader_index_patterns": [ "tsdb-index-*" ],
594+
"remote_cluster": "leader_cluster",
595+
"read_poll_timeout": "1000ms",
596+
"follow_index_pattern": "{{leader_index}}"
597+
}""");
598+
assertOK(client().performRequest(createAutoFollowRequest));
599+
600+
try (RestClient leaderClient = buildLeaderClient()) {
601+
String now = DateFormatter.forPattern(FormatNames.STRICT_DATE_OPTIONAL_TIME.getName()).format(Instant.now());
602+
603+
// Index a document on the leader index, this should trigger an ILM rollover.
604+
// This will ensure that 'index.lifecycle.indexing_complete' is set.
605+
index(leaderClient, dataStream, "", "@timestamp", now, "volume", 11.0, "metricset", randomAlphaOfLength(5));
606+
607+
String backingIndexName = getDataStreamBackingIndexNames(leaderClient, "tsdb-index-cpu").get(0);
608+
assertBusy(() -> assertOK(client().performRequest(new Request("HEAD", "/" + backingIndexName))));
609+
610+
assertBusy(() -> {
611+
Map<String, Object> indexExplanation = explainIndex(client(), backingIndexName);
612+
assertThat(
613+
"index must wait in the " + WaitUntilTimeSeriesEndTimePassesStep.NAME + " until its end time lapses",
614+
indexExplanation.get("step"),
615+
is(WaitUntilTimeSeriesEndTimePassesStep.NAME)
616+
);
617+
618+
assertThat(indexExplanation.get("step_info"), is(notNullValue()));
619+
assertThat(
620+
(String) ((Map<String, Object>) indexExplanation.get("step_info")).get("message"),
621+
containsString("Waiting until the index's time series end time lapses")
622+
);
623+
}, 30, TimeUnit.SECONDS);
624+
625+
int initialLeaderDocCount = getDocCount(leaderClient, backingIndexName);
626+
627+
// Add more documents to the leader index while it's in WaitUntilTimeSeriesEndTimePassesStep
628+
String futureTimestamp = DateFormatter.forPattern(FormatNames.STRICT_DATE_OPTIONAL_TIME.getName())
629+
.format(Instant.now().plusSeconds(30));
630+
631+
for (int i = 0; i < 5; i++) {
632+
index(leaderClient, dataStream, "", "@timestamp", futureTimestamp, "volume", 20.0 + i, "metricset", "test-sync-" + i);
633+
}
634+
635+
// Verify that new documents are synced to follower while in WaitUntilTimeSeriesEndTimePassesStep
636+
assertBusy(() -> {
637+
int currentLeaderDocCount = getDocCount(leaderClient, backingIndexName);
638+
int currentFollowerDocCount = getDocCount(client(), backingIndexName);
639+
640+
assertThat(
641+
"Leader should have more documents than initially",
642+
currentLeaderDocCount,
643+
greaterThan(initialLeaderDocCount)
644+
);
645+
assertThat("Follower should sync new documents from leader", currentFollowerDocCount, equalTo(currentLeaderDocCount));
646+
647+
// Also verify the step is still WaitUntilTimeSeriesEndTimePassesStep
648+
assertThat(
649+
"Index should still be in WaitUntilTimeSeriesEndTimePassesStep",
650+
explainIndex(client(), backingIndexName).get("step"),
651+
is(WaitUntilTimeSeriesEndTimePassesStep.NAME)
652+
);
653+
}, 30, TimeUnit.SECONDS);
654+
}
655+
}
656+
}
657+
536658
private void configureRemoteClusters(String name, String leaderRemoteClusterSeed) throws IOException {
537659
logger.info("Configuring leader remote cluster [{}]", leaderRemoteClusterSeed);
538660
Request request = new Request("PUT", "/_cluster/settings");
@@ -839,4 +961,24 @@ private static String getShrinkIndexName(RestClient client, String originalIndex
839961
: "lifecycle execution state must contain the target shrink index name for index [" + originalIndex + "]";
840962
return shrunkenIndexName[0];
841963
}
964+
965+
private static Map<String, Object> explainIndex(RestClient client, String indexName) throws IOException {
966+
Request explainRequest = new Request("GET", indexName + "/_ilm/explain");
967+
Response response = client.performRequest(explainRequest);
968+
Map<String, Object> responseMap;
969+
try (InputStream is = response.getEntity().getContent()) {
970+
responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true);
971+
}
972+
973+
@SuppressWarnings("unchecked")
974+
Map<String, Map<String, Object>> indexResponse = ((Map<String, Map<String, Object>>) responseMap.get("indices"));
975+
return indexResponse.get(indexName);
976+
}
977+
978+
private static int getDocCount(RestClient client, String indexName) throws IOException {
979+
Request countRequest = new Request("GET", "/" + indexName + "/_count");
980+
Response response = client.performRequest(countRequest);
981+
Map<String, Object> result = entityAsMap(response);
982+
return (int) result.get("count");
983+
}
842984
}

0 commit comments

Comments
 (0)