Skip to content

Commit a3030c5

Browse files
martijnvggwbrown
andcommitted
[ILM] Add unfollow action (#36970)
This change adds the unfollow action for CCR follower indices. This is needed for the shrink action in case an index is a follower index. This will give the follower index the opportunity to fully catch up with the leader index, pause index following and unfollow the leader index. After this the shrink action can safely perform the ilm shrink. The unfollow action needs to be added to the hot phase and acts as barrier for going to the next phase (warm or delete phases), so that follower indices are being unfollowed properly before indices are expected to go in read-only mode. This allows the force merge action to execute its steps safely. The unfollow action has three steps: * `wait-for-indexing-complete` step: waits for the index in question to get the `index.lifecycle.indexing_complete` setting be set to `true` * `wait-for-follow-shard-tasks` step: waits for all the shard follow tasks for the index being handled to report that the leader shard global checkpoint is equal to the follower shard global checkpoint. * `pause-follower-index` step: Pauses index following, necessary to unfollow * `close-follower-index` step: Closes the index, necessary to unfollow * `unfollow-follower-index` step: Actually unfollows the index using the CCR Unfollow API * `open-follower-index` step: Reopens the index now that it is a normal index * `wait-for-yellow` step: Waits for primary shards to be allocated after reopening the index to ensure the index is ready for the next step In the case of the last two steps, if the index in being handled is a regular index then the steps acts as a no-op. Relates to #34648 Co-authored-by: Martijn van Groningen <martijn.v.groningen@gmail.com> Co-authored-by: Gordon Brown <gordon.brown@elastic.co>
1 parent a2bdfb9 commit a3030c5

File tree

39 files changed

+2387
-31
lines changed

39 files changed

+2387
-31
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/indexlifecycle/IndexLifecycleNamedXContentProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
5656
FreezeAction::parse),
5757
new NamedXContentRegistry.Entry(LifecycleAction.class,
5858
new ParseField(SetPriorityAction.NAME),
59-
SetPriorityAction::parse)
59+
SetPriorityAction::parse),
60+
new NamedXContentRegistry.Entry(LifecycleAction.class,
61+
new ParseField(UnfollowAction.NAME),
62+
UnfollowAction::parse)
6063
);
6164
}
6265
}

client/rest-high-level/src/main/java/org/elasticsearch/client/indexlifecycle/LifecyclePolicy.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ public class LifecyclePolicy implements ToXContentObject {
5757
throw new IllegalArgumentException("ordered " + PHASES_FIELD.getPreferredName() + " are not supported");
5858
}, PHASES_FIELD);
5959

60-
ALLOWED_ACTIONS.put("hot", Sets.newHashSet(SetPriorityAction.NAME, RolloverAction.NAME));
61-
ALLOWED_ACTIONS.put("warm", Sets.newHashSet(SetPriorityAction.NAME, AllocateAction.NAME, ForceMergeAction.NAME,
60+
ALLOWED_ACTIONS.put("hot", Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, RolloverAction.NAME));
61+
ALLOWED_ACTIONS.put("warm", Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, AllocateAction.NAME, ForceMergeAction.NAME,
6262
ReadOnlyAction.NAME, ShrinkAction.NAME));
63-
ALLOWED_ACTIONS.put("cold", Sets.newHashSet(SetPriorityAction.NAME, AllocateAction.NAME, FreezeAction.NAME));
63+
ALLOWED_ACTIONS.put("cold", Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, AllocateAction.NAME, FreezeAction.NAME));
6464
ALLOWED_ACTIONS.put("delete", Sets.newHashSet(DeleteAction.NAME));
6565
}
6666

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.indexlifecycle;
21+
22+
import org.elasticsearch.common.Strings;
23+
import org.elasticsearch.common.xcontent.ObjectParser;
24+
import org.elasticsearch.common.xcontent.ToXContent;
25+
import org.elasticsearch.common.xcontent.ToXContentObject;
26+
import org.elasticsearch.common.xcontent.XContentBuilder;
27+
import org.elasticsearch.common.xcontent.XContentParser;
28+
29+
import java.io.IOException;
30+
31+
public class UnfollowAction implements LifecycleAction, ToXContentObject {
32+
public static final String NAME = "unfollow";
33+
34+
private static final ObjectParser<UnfollowAction, Void> PARSER = new ObjectParser<>(NAME, UnfollowAction::new);
35+
36+
public UnfollowAction() {}
37+
38+
@Override
39+
public String getName() {
40+
return NAME;
41+
}
42+
43+
public static UnfollowAction parse(XContentParser parser) {
44+
return PARSER.apply(parser, null);
45+
}
46+
47+
@Override
48+
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
49+
builder.startObject();
50+
builder.endObject();
51+
return builder;
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return 36970;
57+
}
58+
59+
@Override
60+
public boolean equals(Object obj) {
61+
if (obj == null) {
62+
return false;
63+
}
64+
if (obj.getClass() != getClass()) {
65+
return false;
66+
}
67+
return true;
68+
}
69+
70+
@Override
71+
public String toString() {
72+
return Strings.toString(this);
73+
}
74+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/IndexLifecycleIT.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.elasticsearch.client.indexlifecycle.ShrinkAction;
4949
import org.elasticsearch.client.indexlifecycle.StartILMRequest;
5050
import org.elasticsearch.client.indexlifecycle.StopILMRequest;
51+
import org.elasticsearch.client.indexlifecycle.UnfollowAction;
5152
import org.elasticsearch.common.settings.Settings;
5253
import org.elasticsearch.common.unit.TimeValue;
5354
import org.hamcrest.Matchers;
@@ -144,19 +145,20 @@ public void testStartStopILM() throws Exception {
144145

145146
public void testExplainLifecycle() throws Exception {
146147
Map<String, Phase> lifecyclePhases = new HashMap<>();
147-
Map<String, LifecycleAction> hotActions = Collections.singletonMap(
148-
RolloverAction.NAME,
149-
new RolloverAction(null, TimeValue.timeValueHours(50 * 24), null));
148+
Map<String, LifecycleAction> hotActions = new HashMap<>();
149+
hotActions.put(RolloverAction.NAME, new RolloverAction(null, TimeValue.timeValueHours(50 * 24), null));
150150
Phase hotPhase = new Phase("hot", randomFrom(TimeValue.ZERO, null), hotActions);
151151
lifecyclePhases.put("hot", hotPhase);
152152

153153
Map<String, LifecycleAction> warmActions = new HashMap<>();
154+
warmActions.put(UnfollowAction.NAME, new UnfollowAction());
154155
warmActions.put(AllocateAction.NAME, new AllocateAction(null, null, null, Collections.singletonMap("_name", "node-1")));
155156
warmActions.put(ShrinkAction.NAME, new ShrinkAction(1));
156157
warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1000));
157158
lifecyclePhases.put("warm", new Phase("warm", TimeValue.timeValueSeconds(1000), warmActions));
158159

159160
Map<String, LifecycleAction> coldActions = new HashMap<>();
161+
coldActions.put(UnfollowAction.NAME, new UnfollowAction());
160162
coldActions.put(AllocateAction.NAME, new AllocateAction(0, null, null, null));
161163
lifecyclePhases.put("cold", new Phase("cold", TimeValue.timeValueSeconds(2000), coldActions));
162164

client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.elasticsearch.client.indexlifecycle.RolloverAction;
5757
import org.elasticsearch.client.indexlifecycle.SetPriorityAction;
5858
import org.elasticsearch.client.indexlifecycle.ShrinkAction;
59+
import org.elasticsearch.client.indexlifecycle.UnfollowAction;
5960
import org.elasticsearch.cluster.ClusterName;
6061
import org.elasticsearch.common.CheckedFunction;
6162
import org.elasticsearch.common.bytes.BytesReference;
@@ -645,7 +646,7 @@ public void testDefaultNamedXContents() {
645646

646647
public void testProvidedNamedXContents() {
647648
List<NamedXContentRegistry.Entry> namedXContents = RestHighLevelClient.getProvidedNamedXContents();
648-
assertEquals(19, namedXContents.size());
649+
assertEquals(20, namedXContents.size());
649650
Map<Class<?>, Integer> categories = new HashMap<>();
650651
List<String> names = new ArrayList<>();
651652
for (NamedXContentRegistry.Entry namedXContent : namedXContents) {
@@ -669,7 +670,8 @@ public void testProvidedNamedXContents() {
669670
assertTrue(names.contains(MeanReciprocalRank.NAME));
670671
assertTrue(names.contains(DiscountedCumulativeGain.NAME));
671672
assertTrue(names.contains(ExpectedReciprocalRank.NAME));
672-
assertEquals(Integer.valueOf(8), categories.get(LifecycleAction.class));
673+
assertEquals(Integer.valueOf(9), categories.get(LifecycleAction.class));
674+
assertTrue(names.contains(UnfollowAction.NAME));
673675
assertTrue(names.contains(AllocateAction.NAME));
674676
assertTrue(names.contains(DeleteAction.NAME));
675677
assertTrue(names.contains(ForceMergeAction.NAME));

client/rest-high-level/src/test/java/org/elasticsearch/client/indexlifecycle/GetLifecyclePolicyResponseTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ protected NamedXContentRegistry xContentRegistry() {
6868
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse),
6969
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse),
7070
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse),
71-
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse)
71+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse),
72+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse)
7273
));
7374
return new NamedXContentRegistry(entries);
7475
}

client/rest-high-level/src/test/java/org/elasticsearch/client/indexlifecycle/LifecyclePolicyMetadataTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ protected NamedXContentRegistry xContentRegistry() {
6464
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse),
6565
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse),
6666
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse),
67-
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse)
67+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse),
68+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse)
6869
));
6970
return new NamedXContentRegistry(entries);
7071
}

client/rest-high-level/src/test/java/org/elasticsearch/client/indexlifecycle/LifecyclePolicyTests.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@
3939
import static org.hamcrest.Matchers.equalTo;
4040

4141
public class LifecyclePolicyTests extends AbstractXContentTestCase<LifecyclePolicy> {
42-
private static final Set<String> VALID_HOT_ACTIONS = Sets.newHashSet(SetPriorityAction.NAME, RolloverAction.NAME);
43-
private static final Set<String> VALID_WARM_ACTIONS = Sets.newHashSet(SetPriorityAction.NAME, AllocateAction.NAME,
42+
private static final Set<String> VALID_HOT_ACTIONS = Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, RolloverAction.NAME);
43+
private static final Set<String> VALID_WARM_ACTIONS = Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, AllocateAction.NAME,
4444
ForceMergeAction.NAME, ReadOnlyAction.NAME, ShrinkAction.NAME);
45-
private static final Set<String> VALID_COLD_ACTIONS = Sets.newHashSet(SetPriorityAction.NAME, AllocateAction.NAME, FreezeAction.NAME);
45+
private static final Set<String> VALID_COLD_ACTIONS = Sets.newHashSet(UnfollowAction.NAME, SetPriorityAction.NAME, AllocateAction.NAME,
46+
FreezeAction.NAME);
4647
private static final Set<String> VALID_DELETE_ACTIONS = Sets.newHashSet(DeleteAction.NAME);
4748

4849
private String lifecycleName;
@@ -68,7 +69,8 @@ protected NamedXContentRegistry xContentRegistry() {
6869
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse),
6970
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse),
7071
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse),
71-
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse)
72+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse),
73+
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse)
7274
));
7375
return new NamedXContentRegistry(entries);
7476
}
@@ -213,6 +215,8 @@ public static LifecyclePolicy createRandomPolicy(String lifecycleName) {
213215
return new FreezeAction();
214216
case SetPriorityAction.NAME:
215217
return SetPriorityActionTests.randomInstance();
218+
case UnfollowAction.NAME:
219+
return new UnfollowAction();
216220
default:
217221
throw new IllegalArgumentException("invalid action [" + action + "]");
218222
}};
@@ -246,6 +250,8 @@ private LifecycleAction getTestAction(String actionName) {
246250
return new FreezeAction();
247251
case SetPriorityAction.NAME:
248252
return SetPriorityActionTests.randomInstance();
253+
case UnfollowAction.NAME:
254+
return new UnfollowAction();
249255
default:
250256
throw new IllegalArgumentException("unsupported phase action [" + actionName + "]");
251257
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.indexlifecycle;
21+
22+
import org.elasticsearch.common.xcontent.XContentParser;
23+
import org.elasticsearch.test.AbstractXContentTestCase;
24+
25+
import java.io.IOException;
26+
27+
public class UnfollowActionTests extends AbstractXContentTestCase<UnfollowAction> {
28+
29+
@Override
30+
protected UnfollowAction createTestInstance() {
31+
return new UnfollowAction();
32+
}
33+
34+
@Override
35+
protected UnfollowAction doParseInstance(XContentParser parser) throws IOException {
36+
return UnfollowAction.parse(parser);
37+
}
38+
39+
@Override
40+
protected boolean supportsUnknownFields() {
41+
return false;
42+
}
43+
}

docs/reference/ilm/policy-definitions.asciidoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,19 @@ The below list shows the actions which are available in each phase.
8787
* Hot
8888
- <<ilm-set-priority-action,Set Priority>>
8989
- <<ilm-rollover-action,Rollover>>
90+
- <<ilm-unfollow-action,Unfollow>>
9091
* Warm
9192
- <<ilm-set-priority-action,Set Priority>>
9293
- <<ilm-allocate-action,Allocate>>
9394
- <<ilm-readonly-action,Read-Only>>
9495
- <<ilm-forcemerge-action,Force Merge>>
9596
- <<ilm-shrink-action,Shrink>>
97+
- <<ilm-unfollow-action,Unfollow>>
9698
* Cold
9799
- <<ilm-set-priority-action,Set Priority>>
98100
- <<ilm-allocate-action,Allocate>>
99101
- <<ilm-freeze-action,Freeze>>
102+
- <<ilm-unfollow-action,Unfollow>>
100103
* Delete
101104
- <<ilm-delete-action,Delete>>
102105

@@ -616,6 +619,43 @@ PUT _ilm/policy/my_policy
616619
--------------------------------------------------
617620
// CONSOLE
618621

622+
[[ilm-unfollow-action]]
623+
==== Unfollow
624+
625+
This action turns a {ref}/ccr-apis.html[ccr] follower index
626+
into a regular index. This can be desired when moving follower
627+
indices into the next phase. Also certain actions like shrink
628+
and rollover can then be performed safely on follower indices.
629+
630+
If the unfollow action encounters a follower index then
631+
the following operations will be performed on it:
632+
633+
* Pauses indexing following for the follower index.
634+
* Closes the follower index.
635+
* Unfollows the follower index.
636+
* Opens the follower index (which is at this point is a regular index).
637+
638+
The unfollow action does not have any options and
639+
if it encounters a non follower index, then the
640+
unfollow action leaves that index untouched and
641+
lets the next action operate on this index.
642+
643+
[source,js]
644+
--------------------------------------------------
645+
PUT _ilm/policy/my_policy
646+
{
647+
"policy": {
648+
"phases": {
649+
"hot": {
650+
"actions": {
651+
"unfollow" : {}
652+
}
653+
}
654+
}
655+
}
656+
}
657+
--------------------------------------------------
658+
// CONSOLE
619659

620660
=== Full Policy
621661

0 commit comments

Comments
 (0)