Browse Source

Adding best_compression (#49974)

This commit adds a `codec` parameter to the ILM `forcemerge` action. When setting the codec to `best_compression` ILM will close the index, then update the codec setting, re-open the index, and finally perform a force merge.
Sivagurunathan Velayutham 5 years ago
parent
commit
763480ee12

+ 50 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CloseIndexStep.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.close.CloseIndexRequest;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateObserver;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+
+/**
+ * Invokes a close step on a single index.
+ */
+
+public class CloseIndexStep extends AsyncActionStep {
+    public static final String NAME = "close-index";
+
+    CloseIndexStep(StepKey key, StepKey nextStepKey, Client client) {
+        super(key, nextStepKey, client);
+    }
+
+    @Override
+    public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState,
+                              ClusterStateObserver observer, Listener listener) {
+        if (indexMetaData.getState() == IndexMetaData.State.OPEN) {
+            CloseIndexRequest request = new CloseIndexRequest(indexMetaData.getIndex().getName());
+            getClient().admin().indices()
+                .close(request, ActionListener.wrap(closeIndexResponse -> {
+                    if (closeIndexResponse.isAcknowledged() == false) {
+                        throw new ElasticsearchException("close index request failed to be acknowledged");
+                    }
+                    listener.onResponse(true);
+                }, listener::onFailure));
+        }
+        else {
+            listener.onResponse(true);
+        }
+    }
+
+    @Override
+    public boolean isRetryable() {
+        return true;
+    }
+}

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

@@ -5,8 +5,11 @@
  */
 package org.elasticsearch.xpack.core.ilm;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -15,10 +18,12 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.codec.CodecService;
+import org.elasticsearch.index.engine.EngineConfig;
 import org.elasticsearch.xpack.core.ilm.Step.StepKey;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -28,42 +33,62 @@ import java.util.Objects;
 public class ForceMergeAction implements LifecycleAction {
     public static final String NAME = "forcemerge";
     public static final ParseField MAX_NUM_SEGMENTS_FIELD = new ParseField("max_num_segments");
+    public static final ParseField CODEC = new ParseField("index_codec");
 
     private static final ConstructingObjectParser<ForceMergeAction, Void> PARSER = new ConstructingObjectParser<>(NAME,
         false, a -> {
         int maxNumSegments = (int) a[0];
-        return new ForceMergeAction(maxNumSegments);
+        String codec = a[1] != null ? (String) a[1] : null;
+        return new ForceMergeAction(maxNumSegments, codec);
     });
 
     static {
         PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_NUM_SEGMENTS_FIELD);
+        PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CODEC);
     }
 
     private final int maxNumSegments;
+    private final String codec;
 
     public static ForceMergeAction parse(XContentParser parser) {
         return PARSER.apply(parser, null);
     }
 
-    public ForceMergeAction(int maxNumSegments) {
+    public ForceMergeAction(int maxNumSegments, @Nullable String codec) {
         if (maxNumSegments <= 0) {
             throw new IllegalArgumentException("[" + MAX_NUM_SEGMENTS_FIELD.getPreferredName()
                 + "] must be a positive integer");
         }
         this.maxNumSegments = maxNumSegments;
+        if (codec != null && CodecService.BEST_COMPRESSION_CODEC.equals(codec) == false) {
+            throw new IllegalArgumentException("unknown index codec: [" + codec + "]");
+        }
+        this.codec = codec;
     }
 
     public ForceMergeAction(StreamInput in) throws IOException {
         this.maxNumSegments = in.readVInt();
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+            this.codec = in.readOptionalString();
+        } else {
+            this.codec = null;
+        }
     }
 
     public int getMaxNumSegments() {
         return maxNumSegments;
     }
 
+    public String getCodec() {
+        return this.codec;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeVInt(maxNumSegments);
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+            out.writeOptionalString(codec);
+        }
     }
 
     @Override
@@ -80,6 +105,9 @@ public class ForceMergeAction implements LifecycleAction {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field(MAX_NUM_SEGMENTS_FIELD.getPreferredName(), maxNumSegments);
+        if (codec != null) {
+            builder.field(CODEC.getPreferredName(), codec);
+        }
         builder.endObject();
         return builder;
     }
@@ -87,20 +115,48 @@ public class ForceMergeAction implements LifecycleAction {
     @Override
     public List<Step> toSteps(Client client, String phase, Step.StepKey nextStepKey) {
         Settings readOnlySettings = Settings.builder().put(IndexMetaData.SETTING_BLOCKS_WRITE, true).build();
+        Settings bestCompressionSettings = Settings.builder()
+            .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), CodecService.BEST_COMPRESSION_CODEC).build();
 
         StepKey readOnlyKey = new StepKey(phase, NAME, ReadOnlyAction.NAME);
         StepKey forceMergeKey = new StepKey(phase, NAME, ForceMergeStep.NAME);
         StepKey countKey = new StepKey(phase, NAME, SegmentCountStep.NAME);
 
+        StepKey closeKey = new StepKey(phase, NAME, CloseIndexStep.NAME);
+        StepKey openKey = new StepKey(phase, NAME, OpenIndexStep.NAME);
+        StepKey waitForGreenIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME);
+        StepKey updateCompressionKey = new StepKey(phase, NAME, UpdateSettingsStep.NAME);
+
         UpdateSettingsStep readOnlyStep = new UpdateSettingsStep(readOnlyKey, forceMergeKey, client, readOnlySettings);
         ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeKey, countKey, client, maxNumSegments);
+        CloseIndexStep closeIndexStep = new CloseIndexStep(closeKey, updateCompressionKey, client);
+        ForceMergeStep forceMergeStepForBestCompression = new ForceMergeStep(forceMergeKey, closeKey, client, maxNumSegments);
+        UpdateSettingsStep updateBestCompressionSettings = new UpdateSettingsStep(updateCompressionKey,
+            openKey, client, bestCompressionSettings);
+        OpenIndexStep openIndexStep = new OpenIndexStep(openKey, waitForGreenIndexKey, client);
+        WaitForIndexColorStep waitForIndexGreenStep = new WaitForIndexColorStep(waitForGreenIndexKey,
+            forceMergeKey, ClusterHealthStatus.GREEN);
         SegmentCountStep segmentCountStep = new SegmentCountStep(countKey, nextStepKey, client, maxNumSegments);
-        return Arrays.asList(readOnlyStep, forceMergeStep, segmentCountStep);
+
+        List<Step> mergeSteps = new ArrayList<>();
+        mergeSteps.add(readOnlyStep);
+
+        if (codec != null && codec.equals(CodecService.BEST_COMPRESSION_CODEC)) {
+            mergeSteps.add(forceMergeStepForBestCompression);
+            mergeSteps.add(closeIndexStep);
+            mergeSteps.add(updateBestCompressionSettings);
+            mergeSteps.add(openIndexStep);
+            mergeSteps.add(waitForIndexGreenStep);
+        }
+
+        mergeSteps.add(forceMergeStep);
+        mergeSteps.add(segmentCountStep);
+        return mergeSteps;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(maxNumSegments);
+        return Objects.hash(maxNumSegments, codec);
     }
 
     @Override
@@ -112,7 +168,8 @@ public class ForceMergeAction implements LifecycleAction {
             return false;
         }
         ForceMergeAction other = (ForceMergeAction) obj;
-        return Objects.equals(maxNumSegments, other.maxNumSegments);
+        return Objects.equals(this.maxNumSegments, other.maxNumSegments)
+            && Objects.equals(this.codec, other.codec);
     }
 
     @Override

+ 52 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OpenIndexStep.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateObserver;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+
+/**
+ * Invokes a open step on a single index.
+ */
+
+final class OpenIndexStep extends AsyncActionStep {
+
+    static final String NAME = "open-index";
+
+    OpenIndexStep(StepKey key, StepKey nextStepKey, Client client) {
+        super(key, nextStepKey, client);
+    }
+
+    @Override
+    public void performAction(IndexMetaData indexMetaData, ClusterState currentClusterState,
+                              ClusterStateObserver observer, Listener listener) {
+        if (indexMetaData.getState() == IndexMetaData.State.CLOSE) {
+            OpenIndexRequest request = new OpenIndexRequest(indexMetaData.getIndex().getName());
+            getClient().admin().indices()
+                .open(request,
+                    ActionListener.wrap(openIndexResponse -> {
+                        if (openIndexResponse.isAcknowledged() == false) {
+                            throw new ElasticsearchException("open index request failed to be acknowledged");
+                        }
+                        listener.onResponse(true);
+                    }, listener::onFailure));
+
+        } else {
+            listener.onResponse(true);
+        }
+    }
+
+    @Override
+    public boolean isRetryable() {
+        return true;
+    }
+}

+ 164 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java

@@ -0,0 +1,164 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import com.carrotsearch.hppc.cursors.ObjectCursor;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.cluster.routing.IndexRoutingTable;
+import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
+import org.elasticsearch.cluster.routing.RoutingTable;
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.Index;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Wait Step for index based on color
+ */
+
+class WaitForIndexColorStep extends ClusterStateWaitStep {
+
+    static final String NAME = "wait-for-index-color";
+
+    private final ClusterHealthStatus color;
+
+    WaitForIndexColorStep(StepKey key, StepKey nextStepKey, ClusterHealthStatus color) {
+        super(key, nextStepKey);
+        this.color = color;
+    }
+
+    public ClusterHealthStatus getColor() {
+        return this.color;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), this.color);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        WaitForIndexColorStep other = (WaitForIndexColorStep) obj;
+        return super.equals(obj) && Objects.equals(this.color, other.color);
+    }
+
+    @Override
+    public Result isConditionMet(Index index, ClusterState clusterState) {
+        RoutingTable routingTable = clusterState.routingTable();
+        IndexRoutingTable indexRoutingTable = routingTable.index(index);
+        Result result;
+        switch (this.color) {
+            case GREEN:
+                result = waitForGreen(indexRoutingTable);
+                break;
+            case YELLOW:
+                result = waitForYellow(indexRoutingTable);
+                break;
+            case RED:
+                result = waitForRed(indexRoutingTable);
+                break;
+            default:
+                result = new Result(false, new Info("no index color match"));
+                break;
+        }
+        return result;
+    }
+
+    @Override
+    public boolean isRetryable() {
+        return true;
+    }
+
+    private Result waitForRed(IndexRoutingTable indexRoutingTable) {
+        if (indexRoutingTable == null) {
+            return new Result(true, new Info("index is red"));
+        }
+        return new Result(false, new Info("index is not red"));
+    }
+
+    private Result waitForYellow(IndexRoutingTable indexRoutingTable) {
+        if (indexRoutingTable == null) {
+            return new Result(false, new Info("index is red; no indexRoutingTable"));
+        }
+
+        boolean indexIsAtLeastYellow = indexRoutingTable.allPrimaryShardsActive();
+        if (indexIsAtLeastYellow) {
+            return new Result(true, null);
+        } else {
+            return new Result(false, new Info("index is red; not all primary shards are active"));
+        }
+    }
+
+    private Result waitForGreen(IndexRoutingTable indexRoutingTable) {
+        if (indexRoutingTable == null) {
+            return new Result(false, new Info("index is red; no indexRoutingTable"));
+        }
+
+        if (indexRoutingTable.allPrimaryShardsActive()) {
+            for (ObjectCursor<IndexShardRoutingTable> shardRouting : indexRoutingTable.getShards().values()) {
+                boolean replicaIndexIsGreen = shardRouting.value.replicaShards().stream().allMatch(ShardRouting::active);
+                if (replicaIndexIsGreen == false) {
+                    return new Result(false, new Info("index is yellow; not all replica shards are active"));
+                }
+            }
+            return new Result(true, null);
+        }
+
+        return new Result(false, new Info("index is not green; not all shards are active"));
+    }
+
+    static final class Info implements ToXContentObject {
+
+        static final ParseField MESSAGE_FIELD = new ParseField("message");
+
+        private final String message;
+
+        Info(String message) {
+            this.message = message;
+        }
+
+        String getMessage() {
+            return message;
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(MESSAGE_FIELD.getPreferredName(), message);
+            builder.endObject();
+            return builder;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null) {
+                return false;
+            }
+            if (getClass() != o.getClass()) {
+                return false;
+            }
+            Info info = (Info) o;
+            return Objects.equals(getMessage(), info.getMessage());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(getMessage());
+        }
+    }
+}

+ 150 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java

@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.close.CloseIndexRequest;
+import org.elasticsearch.action.admin.indices.close.CloseIndexResponse;
+import org.elasticsearch.client.AdminClient;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.client.IndicesAdminClient;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.junit.Before;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class CloseIndexStepTests extends AbstractStepTestCase<CloseIndexStep> {
+
+    private Client client;
+
+    @Before
+    public void setup() {
+        client = Mockito.mock(Client.class);
+    }
+
+    @Override
+    protected CloseIndexStep createRandomInstance() {
+        return new CloseIndexStep(randomStepKey(), randomStepKey(), client);
+    }
+
+    @Override
+    protected CloseIndexStep mutateInstance(CloseIndexStep instance) {
+        Step.StepKey key = instance.getKey();
+        Step.StepKey nextKey = instance.getNextStepKey();
+
+        switch (between(0, 1)) {
+            case 0:
+                key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            case 1:
+                nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            default:
+                throw new AssertionError("Illegal randomisation branch");
+        }
+
+        return new CloseIndexStep(key, nextKey, client);
+    }
+
+    @Override
+    protected CloseIndexStep copyInstance(CloseIndexStep instance) {
+        return new CloseIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient());
+    }
+
+    public void testPerformAction() {
+        IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT))
+            .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build();
+
+        CloseIndexStep step = createRandomInstance();
+
+        AdminClient adminClient = Mockito.mock(AdminClient.class);
+        IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class);
+
+        Mockito.when(client.admin()).thenReturn(adminClient);
+        Mockito.when(adminClient.indices()).thenReturn(indicesClient);
+
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0];
+            @SuppressWarnings("unchecked")
+            ActionListener<CloseIndexResponse> listener = (ActionListener<CloseIndexResponse>) invocation.getArguments()[1];
+            assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()}));
+            listener.onResponse(new CloseIndexResponse(true, true,
+                Collections.singletonList(new CloseIndexResponse.IndexResult(indexMetaData.getIndex()))));
+            return null;
+        }).when(indicesClient).close(Mockito.any(), Mockito.any());
+
+        SetOnce<Boolean> actionCompleted = new SetOnce<>();
+
+        step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() {
+
+            @Override
+            public void onResponse(boolean complete) {
+                actionCompleted.set(complete);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                throw new AssertionError("Unexpected method call", e);
+            }
+        });
+
+        assertEquals(true, actionCompleted.get());
+        Mockito.verify(client, Mockito.only()).admin();
+        Mockito.verify(adminClient, Mockito.only()).indices();
+        Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any());
+    }
+
+
+    public void testPerformActionFailure() {
+        IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT))
+            .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build();
+
+        CloseIndexStep step = createRandomInstance();
+        Exception exception = new RuntimeException();
+        AdminClient adminClient = Mockito.mock(AdminClient.class);
+        IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class);
+
+        Mockito.when(client.admin()).thenReturn(adminClient);
+        Mockito.when(adminClient.indices()).thenReturn(indicesClient);
+
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            CloseIndexRequest request = (CloseIndexRequest) invocation.getArguments()[0];
+            @SuppressWarnings("unchecked")
+            ActionListener<CloseIndexResponse> listener = (ActionListener<CloseIndexResponse>) invocation.getArguments()[1];
+            assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()}));
+            listener.onFailure(exception);
+            return null;
+        }).when(indicesClient).close(Mockito.any(), Mockito.any());
+
+        SetOnce<Boolean> exceptionThrown = new SetOnce<>();
+
+        step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() {
+
+            @Override
+            public void onResponse(boolean complete) {
+                throw new AssertionError("Unexpected method call");
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                assertSame(exception, e);
+                exceptionThrown.set(true);
+            }
+        });
+
+        assertEquals(true, exceptionThrown.get());
+        Mockito.verify(client, Mockito.only()).admin();
+        Mockito.verify(adminClient, Mockito.only()).indices();
+        Mockito.verify(indicesClient, Mockito.only()).close(Mockito.any(), Mockito.any());
+    }
+}

+ 76 - 17
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java

@@ -13,6 +13,8 @@ import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.index.codec.CodecService;
+import org.elasticsearch.index.engine.EngineConfig;
 import org.elasticsearch.xpack.core.ilm.Step.StepKey;
 
 import java.io.IOException;
@@ -33,14 +35,21 @@ public class ForceMergeActionTests extends AbstractActionTestCase<ForceMergeActi
     }
 
     static ForceMergeAction randomInstance() {
-        return new ForceMergeAction(randomIntBetween(1, 100));
+        return new ForceMergeAction(randomIntBetween(1, 100), createRandomCompressionSettings());
+    }
+
+    static String createRandomCompressionSettings() {
+        if (randomBoolean()) {
+            return null;
+        }
+        return CodecService.BEST_COMPRESSION_CODEC;
     }
 
     @Override
     protected ForceMergeAction mutateInstance(ForceMergeAction instance) {
         int maxNumSegments = instance.getMaxNumSegments();
         maxNumSegments = maxNumSegments + randomIntBetween(1, 10);
-        return new ForceMergeAction(maxNumSegments);
+        return new ForceMergeAction(maxNumSegments, createRandomCompressionSettings());
     }
 
     @Override
@@ -48,21 +57,7 @@ public class ForceMergeActionTests extends AbstractActionTestCase<ForceMergeActi
         return ForceMergeAction::new;
     }
 
-    public void testMissingMaxNumSegments() throws IOException {
-        BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject());
-        XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
-            emptyObject, XContentType.JSON);
-        Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser));
-        assertThat(e.getMessage(), equalTo("Required [max_num_segments]"));
-    }
-
-    public void testInvalidNegativeSegmentNumber() {
-        Exception r = expectThrows(IllegalArgumentException.class, () -> new ForceMergeAction(randomIntBetween(-10, 0)));
-        assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer"));
-    }
-
-    public void testToSteps() {
-        ForceMergeAction instance = createTestInstance();
+    private void assertNonBestCompression(ForceMergeAction instance) {
         String phase = randomAlphaOfLength(5);
         StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10));
         List<Step> steps = instance.toSteps(null, phase, nextStepKey);
@@ -79,4 +74,68 @@ public class ForceMergeActionTests extends AbstractActionTestCase<ForceMergeActi
         assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME)));
         assertThat(thirdStep.getNextStepKey(), equalTo(nextStepKey));
     }
+
+    private void assertBestCompression(ForceMergeAction instance) {
+        String phase = randomAlphaOfLength(5);
+        StepKey nextStepKey = new StepKey(randomAlphaOfLength(10), randomAlphaOfLength(10), randomAlphaOfLength(10));
+        List<Step> steps = instance.toSteps(null, phase, nextStepKey);
+        assertNotNull(steps);
+        assertEquals(8, steps.size());
+        UpdateSettingsStep firstStep = (UpdateSettingsStep) steps.get(0);
+        ForceMergeStep secondStep = (ForceMergeStep) steps.get(1);
+        CloseIndexStep thirdStep = (CloseIndexStep) steps.get(2);
+        UpdateSettingsStep fourthStep = (UpdateSettingsStep) steps.get(3);
+        OpenIndexStep fifthStep = (OpenIndexStep) steps.get(4);
+        WaitForIndexColorStep sixthStep = (WaitForIndexColorStep) steps.get(5);
+        ForceMergeStep seventhStep = (ForceMergeStep) steps.get(6);
+        SegmentCountStep eighthStep = (SegmentCountStep) steps.get(7);
+        assertThat(firstStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ReadOnlyAction.NAME)));
+        assertThat(firstStep.getNextStepKey(), equalTo(secondStep));
+        assertTrue(IndexMetaData.INDEX_BLOCKS_WRITE_SETTING.get(firstStep.getSettings()));
+        assertThat(secondStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeAction.NAME)));
+        assertThat(secondStep.getNextStepKey(), equalTo(thirdStep));
+        assertThat(thirdStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, CloseIndexStep.NAME)));
+        assertThat(thirdStep.getNextStepKey(), equalTo(fourthStep.getKey()));
+        assertThat(fourthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, UpdateSettingsStep.NAME)));
+        assertThat(fourthStep.getSettings().get(EngineConfig.INDEX_CODEC_SETTING.getKey()), equalTo(CodecService.BEST_COMPRESSION_CODEC));
+        assertThat(fourthStep.getNextStepKey(), equalTo(fifthStep.getKey()));
+        assertThat(fifthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, OpenIndexStep.NAME)));
+        assertThat(fifthStep.getNextStepKey(), equalTo(sixthStep));
+        assertThat(sixthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, WaitForIndexColorStep.NAME)));
+        assertThat(sixthStep.getNextStepKey(), equalTo(seventhStep));
+        assertThat(seventhStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, ForceMergeStep.NAME)));
+        assertThat(seventhStep.getNextStepKey(), equalTo(nextStepKey));
+        assertThat(eighthStep.getKey(), equalTo(new StepKey(phase, ForceMergeAction.NAME, SegmentCountStep.NAME)));
+        assertThat(eighthStep.getNextStepKey(), equalTo(nextStepKey));
+    }
+
+    public void testMissingMaxNumSegments() throws IOException {
+        BytesReference emptyObject = BytesReference.bytes(JsonXContent.contentBuilder().startObject().endObject());
+        XContentParser parser = XContentHelper.createParser(null, DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
+            emptyObject, XContentType.JSON);
+        Exception e = expectThrows(IllegalArgumentException.class, () -> ForceMergeAction.parse(parser));
+        assertThat(e.getMessage(), equalTo("Required [max_num_segments]"));
+    }
+
+    public void testInvalidNegativeSegmentNumber() {
+        Exception r = expectThrows(IllegalArgumentException.class, () -> new
+            ForceMergeAction(randomIntBetween(-10, 0), null));
+        assertThat(r.getMessage(), equalTo("[max_num_segments] must be a positive integer"));
+    }
+
+    public void testInvalidCodec() {
+        Exception r = expectThrows(IllegalArgumentException.class, () -> new
+            ForceMergeAction(randomIntBetween(1, 10), "DummyCompressingStoredFields"));
+        assertThat(r.getMessage(), equalTo("unknown index codec: [DummyCompressingStoredFields]"));
+    }
+
+    public void testToSteps() {
+        ForceMergeAction instance = createTestInstance();
+        if (instance.getCodec() != null && CodecService.BEST_COMPRESSION_CODEC.equals(instance.getCodec())) {
+            assertBestCompression(instance);
+        }
+        else {
+            assertNonBestCompression(instance);
+        }
+    }
 }

+ 153 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OpenIndexStepTests.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
+import org.elasticsearch.action.admin.indices.open.OpenIndexResponse;
+import org.elasticsearch.client.AdminClient;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.client.IndicesAdminClient;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.junit.Before;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class OpenIndexStepTests extends AbstractStepTestCase<OpenIndexStep> {
+
+    private Client client;
+
+    @Before
+    public void setup() {
+        client = Mockito.mock(Client.class);
+    }
+
+    @Override
+    protected OpenIndexStep createRandomInstance() {
+        return new OpenIndexStep(randomStepKey(), randomStepKey(), client);
+    }
+
+    @Override
+    protected OpenIndexStep mutateInstance(OpenIndexStep instance) {
+        Step.StepKey key = instance.getKey();
+        Step.StepKey nextKey = instance.getNextStepKey();
+
+        switch (between(0, 1)) {
+            case 0:
+                key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            case 1:
+                nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            default:
+                throw new AssertionError("Illegal randomisation branch");
+        }
+
+        return new OpenIndexStep(key, nextKey, client);
+    }
+
+    @Override
+    protected OpenIndexStep copyInstance(OpenIndexStep instance) {
+        return new OpenIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient());
+    }
+
+    public void testPerformAction() {
+        IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT))
+            .numberOfShards(randomIntBetween(1, 5))
+            .numberOfReplicas(randomIntBetween(0, 5))
+            .state(IndexMetaData.State.CLOSE)
+            .build();
+
+        OpenIndexStep step = createRandomInstance();
+
+        AdminClient adminClient = Mockito.mock(AdminClient.class);
+        IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class);
+
+        Mockito.when(client.admin()).thenReturn(adminClient);
+        Mockito.when(adminClient.indices()).thenReturn(indicesClient);
+
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0];
+            @SuppressWarnings("unchecked")
+            ActionListener<OpenIndexResponse> listener = (ActionListener<OpenIndexResponse>) invocation.getArguments()[1];
+            assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()}));
+            listener.onResponse(new OpenIndexResponse(true, true));
+            return null;
+        }).when(indicesClient).open(Mockito.any(), Mockito.any());
+
+        SetOnce<Boolean> actionCompleted = new SetOnce<>();
+
+        step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() {
+
+            @Override
+            public void onResponse(boolean complete) {
+                actionCompleted.set(complete);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                throw new AssertionError("Unexpected method call", e);
+            }
+        });
+
+        assertEquals(true, actionCompleted.get());
+        Mockito.verify(client, Mockito.only()).admin();
+        Mockito.verify(adminClient, Mockito.only()).indices();
+        Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any());
+    }
+
+
+    public void testPerformActionFailure() {
+        IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT))
+            .numberOfShards(randomIntBetween(1, 5))
+            .numberOfReplicas(randomIntBetween(0, 5))
+            .state(IndexMetaData.State.CLOSE)
+            .build();
+
+        OpenIndexStep step = createRandomInstance();
+        Exception exception = new RuntimeException();
+        AdminClient adminClient = Mockito.mock(AdminClient.class);
+        IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class);
+
+        Mockito.when(client.admin()).thenReturn(adminClient);
+        Mockito.when(adminClient.indices()).thenReturn(indicesClient);
+
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            OpenIndexRequest request = (OpenIndexRequest) invocation.getArguments()[0];
+            @SuppressWarnings("unchecked")
+            ActionListener<OpenIndexResponse> listener = (ActionListener<OpenIndexResponse>) invocation.getArguments()[1];
+            assertThat(request.indices(), equalTo(new String[]{indexMetaData.getIndex().getName()}));
+            listener.onFailure(exception);
+            return null;
+        }).when(indicesClient).open(Mockito.any(), Mockito.any());
+
+        SetOnce<Boolean> exceptionThrown = new SetOnce<>();
+
+        step.performAction(indexMetaData, null, null, new AsyncActionStep.Listener() {
+
+            @Override
+            public void onResponse(boolean complete) {
+                throw new AssertionError("Unexpected method call");
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                assertSame(exception, e);
+                exceptionThrown.set(true);
+            }
+        });
+
+        assertEquals(true, exceptionThrown.get());
+        Mockito.verify(client, Mockito.only()).admin();
+        Mockito.verify(adminClient, Mockito.only()).indices();
+        Mockito.verify(indicesClient, Mockito.only()).open(Mockito.any(), Mockito.any());
+    }
+}

+ 2 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java

@@ -35,7 +35,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase {
         new AllocateAction(2, Collections.singletonMap("node", "node1"),null, null);
     private static final DeleteAction TEST_DELETE_ACTION = new DeleteAction();
     private static final WaitForSnapshotAction TEST_WAIT_FOR_SNAPSHOT_ACTION = new WaitForSnapshotAction("policy");
-    private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1);
+    private static final ForceMergeAction TEST_FORCE_MERGE_ACTION = new ForceMergeAction(1, null);
     private static final RolloverAction TEST_ROLLOVER_ACTION = new RolloverAction(new ByteSizeValue(1), null, null);
     private static final ShrinkAction TEST_SHRINK_ACTION = new ShrinkAction(1);
     private static final ReadOnlyAction TEST_READ_ONLY_ACTION = new ReadOnlyAction();
@@ -493,7 +493,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase {
             case DeleteAction.NAME:
                 return new DeleteAction();
             case ForceMergeAction.NAME:
-                return new ForceMergeAction(1);
+                return new ForceMergeAction(1, null);
             case ReadOnlyAction.NAME:
                 return new ReadOnlyAction();
             case RolloverAction.NAME:

+ 208 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java

@@ -0,0 +1,208 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.cluster.metadata.MetaData;
+import org.elasticsearch.cluster.routing.IndexRoutingTable;
+import org.elasticsearch.cluster.routing.RoutingTable;
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.cluster.routing.ShardRoutingState;
+import org.elasticsearch.cluster.routing.TestShardRouting;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.core.IsNull.notNullValue;
+
+public class WaitForIndexColorStepTests extends AbstractStepTestCase<WaitForIndexColorStep> {
+
+    private static ClusterHealthStatus randomColor() {
+        String[] colors = new String[]{"green", "yellow", "red"};
+        int randomColor = randomIntBetween(0, colors.length - 1);
+        return ClusterHealthStatus.fromString(colors[randomColor]);
+    }
+
+    @Override
+    protected WaitForIndexColorStep createRandomInstance() {
+        StepKey stepKey = randomStepKey();
+        StepKey nextStepKey = randomStepKey();
+        ClusterHealthStatus color = randomColor();
+        return new WaitForIndexColorStep(stepKey, nextStepKey, color);
+    }
+
+    @Override
+    protected WaitForIndexColorStep mutateInstance(WaitForIndexColorStep instance) {
+        StepKey key = instance.getKey();
+        StepKey nextKey = instance.getNextStepKey();
+        ClusterHealthStatus color = instance.getColor(), newColor = randomColor();
+        while (color.equals(newColor)) {
+            newColor = randomColor();
+        }
+
+        switch (between(0, 2)) {
+            case 0:
+                key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            case 1:
+                nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+                break;
+            case 2:
+                color = newColor;
+                break;
+        }
+
+        return new WaitForIndexColorStep(key, nextKey, color);
+    }
+
+    @Override
+    protected WaitForIndexColorStep copyInstance(WaitForIndexColorStep instance) {
+        return new WaitForIndexColorStep(instance.getKey(), instance.getNextStepKey(), instance.getColor());
+    }
+
+    public void testConditionMetForGreen() {
+        IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5))
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(2)
+            .build();
+
+        ShardRouting shardRouting =
+            TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.STARTED);
+        IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex())
+            .addShard(shardRouting).build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().add(indexRoutingTable).build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(true));
+        assertThat(result.getInfomationContext(), nullValue());
+    }
+
+    public void testConditionNotMetForGreen() {
+        IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5))
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        ShardRouting shardRouting =
+            TestShardRouting.newShardRouting("test_index", 0, "1", true, ShardRoutingState.INITIALIZING);
+        IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex())
+            .addShard(shardRouting).build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().add(indexRoutingTable).build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(false));
+        WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext();
+        assertThat(info, notNullValue());
+        assertThat(info.getMessage(), equalTo("index is not green; not all shards are active"));
+    }
+
+    public void testConditionNotMetNoIndexRoutingTable() {
+        IndexMetaData indexMetadata = IndexMetaData.builder(randomAlphaOfLength(5))
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(false));
+        WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext();
+        assertThat(info, notNullValue());
+        assertThat(info.getMessage(), equalTo("index is red; no indexRoutingTable"));
+    }
+
+    public void testConditionMetForYellow() {
+        IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index")
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        ShardRouting shardRouting =
+            TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.STARTED);
+        IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex())
+            .addShard(shardRouting).build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().add(indexRoutingTable).build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(true));
+        assertThat(result.getInfomationContext(), nullValue());
+    }
+
+    public void testConditionNotMetForYellow() {
+        IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index")
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        ShardRouting shardRouting =
+            TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.INITIALIZING);
+        IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetadata.getIndex())
+            .addShard(shardRouting).build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().add(indexRoutingTable).build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(false));
+        WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext();
+        assertThat(info, notNullValue());
+        assertThat(info.getMessage(), equalTo("index is red; not all primary shards are active"));
+    }
+
+    public void testConditionNotMetNoIndexRoutingTableForYellow() {
+        IndexMetaData indexMetadata = IndexMetaData.builder("former-follower-index")
+            .settings(settings(Version.CURRENT))
+            .numberOfShards(1)
+            .numberOfReplicas(0)
+            .build();
+
+        ClusterState clusterState = ClusterState.builder(new ClusterName("_name"))
+            .metaData(MetaData.builder().put(indexMetadata, true).build())
+            .routingTable(RoutingTable.builder().build())
+            .build();
+
+        WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW);
+        ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState);
+        assertThat(result.isComplete(), is(false));
+        WaitForIndexColorStep.Info info = (WaitForIndexColorStep.Info) result.getInfomationContext();
+        assertThat(info, notNullValue());
+        assertThat(info.getMessage(), equalTo("index is red; no indexRoutingTable"));
+    }
+}
+

+ 7 - 4
x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java

@@ -458,7 +458,7 @@ public class TimeSeriesLifecycleActionsIT extends ESRestTestCase {
     }
 
     @SuppressWarnings("unchecked")
-    public void testForceMergeAction() throws Exception {
+    public void forceMergeActionWithCodec(String codec) throws Exception {
         createIndexWithSettings(index, Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
             .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0));
         for (int i = 0; i < randomIntBetween(2, 10); i++) {
@@ -481,8 +481,7 @@ public class TimeSeriesLifecycleActionsIT extends ESRestTestCase {
             }
         };
         assertThat(numSegments.get(), greaterThan(1));
-
-        createNewSingletonPolicy("warm", new ForceMergeAction(1));
+        createNewSingletonPolicy("warm", new ForceMergeAction(1, codec));
         updatePolicy(index, policy);
 
         assertBusy(() -> {
@@ -494,6 +493,10 @@ public class TimeSeriesLifecycleActionsIT extends ESRestTestCase {
         expectThrows(ResponseException.class, this::indexDocument);
     }
 
+    public void testForceMergeAction() throws Exception {
+        forceMergeActionWithCodec(null);
+    }
+
     public void testShrinkAction() throws Exception {
         int numShards = 4;
         int divisor = randomFrom(2, 4);
@@ -1566,7 +1569,7 @@ public class TimeSeriesLifecycleActionsIT extends ESRestTestCase {
         hotActions.put(RolloverAction.NAME,  new RolloverAction(null, null, 1L));
         Map<String, LifecycleAction> warmActions = new HashMap<>();
         warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50));
-        warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1));
+        warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, null));
         warmActions.put(AllocateAction.NAME, new AllocateAction(1, singletonMap("_name", "integTest-1,integTest-2"), null, null));
         warmActions.put(ShrinkAction.NAME, new ShrinkAction(1));
         Map<String, LifecycleAction> coldActions = new HashMap<>();

+ 1 - 1
x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java

@@ -151,7 +151,7 @@ public class TransportPutLifecycleActionTests extends ESTestCase {
                 new Step.StepKey("phase", "set_priority", SetPriorityAction.NAME)));
 
         Map<String, LifecycleAction> actions = new HashMap<>();
-        actions.put("forcemerge", new ForceMergeAction(5));
+        actions.put("forcemerge", new ForceMergeAction(5, null));
         actions.put("freeze", new FreezeAction());
         actions.put("allocate", new AllocateAction(1, null, null, null));
         PhaseExecutionInfo pei = new PhaseExecutionInfo("policy", new Phase("wonky", TimeValue.ZERO, actions), 1, 1);