Преглед изворни кода

ILM don't rollover empty indices (#89557)

Joe Gallo пре 3 година
родитељ
комит
21356162eb

+ 42 - 0
docs/changelog/89557.yaml

@@ -0,0 +1,42 @@
+pr: 89557
+summary: ILM don't rollover empty indices
+area: ILM+SLM
+type: enhancement
+issues:
+ - 86203
+highlight:
+  title: ILM no longer rolls over empty indices
+  body: |-
+    For both new and existing Index Lifecycle Management (ILM) policies,
+    the rollover action will only execute if an index has at least one document.
+
+    For indices with a `max_age` condition that are no longer being written
+    to, this will mean that they will no longer roll over every time their
+    `max_age` is reached.
+
+    A policy can override this behavior, and explicitly opt in to rolling over
+    empty indices, by adding a `"min_docs": 0` condition:
+
+    [source,console]
+    ----
+    PUT _ilm/policy/allow_empty_rollover_policy
+    {
+      "policy": {
+        "phases": {
+          "hot": {
+            "actions": {
+              "rollover" : {
+                "max_age": "7d",
+                "max_size": "100gb",
+                "min_docs": 0
+              }
+            }
+          }
+        }
+      }
+    }
+    ----
+
+    This can also be disabled on a cluster-wide basis by setting
+    `indices.lifecycle.rollover.only_if_has_documents` to `false`.
+  notable: true

+ 10 - 3
docs/reference/ilm/actions/ilm-rollover.asciidoc

@@ -50,7 +50,8 @@ A rollover action must specify at least one max_* condition, it may include zero
 or more min_* conditions. An empty rollover action is invalid.
 
 The index will rollover once any max_* condition is satisfied and all
-min_* conditions are satisfied.
+min_* conditions are satisfied. Note, however, that empty indices are not rolled
+over by default.
 
 // tag::rollover-conditions[]
 `max_age`::
@@ -122,6 +123,12 @@ See notes on `max_primary_shard_docs`.
 
 // end::rollover-conditions[]
 
+IMPORTANT: Empty indices will not be rolled over, even if they have an associated `max_age` that
+would otherwise result in a roll over occurring. A policy can override this behavior, and explicitly
+opt in to rolling over empty indices, by adding a `"min_docs": 0` condition. This can also be
+disabled on a cluster-wide basis by setting `indices.lifecycle.rollover.only_if_has_documents` to
+`false`.
+
 [[ilm-rollover-ex]]
 ==== Example
 
@@ -246,7 +253,7 @@ PUT _ilm/policy/my_policy
 When you specify multiple rollover conditions,
 the index is rolled over when _any_ of the max_* and _all_ of the min_* conditions are met.
 This example rolls the index over if it is at least 7 days old or at least 100 gigabytes,
-but only as long as the index is not empty.
+but only as long as the index contains at least 1000 documents.
 
 [source,console]
 --------------------------------------------------
@@ -259,7 +266,7 @@ PUT _ilm/policy/my_policy
           "rollover" : {
             "max_age": "7d",
             "max_size": "100gb",
-            "min_docs": 1
+            "min_docs": 1000
           }
         }
       }

+ 6 - 0
docs/reference/settings/ilm-settings.asciidoc

@@ -26,6 +26,12 @@ indices. Defaults to `true`.
 (<<dynamic-cluster-setting,Dynamic>>, <<time-units, time unit value>>)
 How often {ilm} checks for indices that meet policy criteria. Defaults to `10m`.
 
+[[indices-lifecycle-rollover-only-if-has-documents]]
+`indices.lifecycle.rollover.only_if_has_documents`::
+(<<dynamic-cluster-setting,Dynamic>>, Boolean)
+Whether ILM will only roll over non-empty indices. If enabled, ILM will only roll over indices
+as long as they contain at least one document. Defaults to `true`.
+
 ==== Index level settings
 These index-level {ilm-init} settings are typically configured through index
 templates. For more information, see <<ilm-gs-create-policy>>.

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

@@ -21,9 +21,10 @@ public class LifecycleSettings {
     public static final String LIFECYCLE_INDEXING_COMPLETE = "index.lifecycle.indexing_complete";
     public static final String LIFECYCLE_ORIGINATION_DATE = "index.lifecycle.origination_date";
     public static final String LIFECYCLE_PARSE_ORIGINATION_DATE = "index.lifecycle.parse_origination_date";
-    public static final String LIFECYCLE_STEP_WAIT_TIME_THRESHOLD = "index.lifecycle.step.wait_time_threshold";
     public static final String LIFECYCLE_HISTORY_INDEX_ENABLED = "indices.lifecycle.history_index_enabled";
     public static final String LIFECYCLE_STEP_MASTER_TIMEOUT = "indices.lifecycle.step.master_timeout";
+    public static final String LIFECYCLE_STEP_WAIT_TIME_THRESHOLD = "index.lifecycle.step.wait_time_threshold";
+    public static final String LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS = "indices.lifecycle.rollover.only_if_has_documents";
 
     public static final String SLM_HISTORY_INDEX_ENABLED = "slm.history_index_enabled";
     public static final String SLM_RETENTION_SCHEDULE = "slm.retention_schedule";
@@ -89,6 +90,13 @@ public class LifecycleSettings {
         Setting.Property.Dynamic,
         Setting.Property.IndexScope
     );
+    public static final Setting<Boolean> LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS_SETTING = Setting.boolSetting(
+        LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS,
+        true,
+        Setting.Property.Dynamic,
+        Setting.Property.NodeScope,
+        Setting.Property.DeprecatedWarning
+    );
 
     public static final Setting<Boolean> SLM_HISTORY_INDEX_ENABLED_SETTING = Setting.boolSetting(
         SLM_HISTORY_INDEX_ENABLED,

+ 35 - 9
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStep.java

@@ -196,6 +196,36 @@ public class WaitForRolloverReadyStep extends AsyncWaitStep {
             rolloverTarget = rolloverAlias;
         }
 
+        // if we should only rollover if not empty, *and* if neither an explicit min_docs nor an explicit min_primary_shard_docs
+        // has been specified on this policy, then inject a default min_docs: 1 condition so that we do not rollover empty indices
+        boolean rolloverOnlyIfHasDocuments = LifecycleSettings.LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS_SETTING.get(metadata.settings());
+        RolloverRequest rolloverRequest = createRolloverRequest(rolloverTarget, masterTimeout, rolloverOnlyIfHasDocuments);
+
+        getClient().admin()
+            .indices()
+            .rolloverIndex(
+                rolloverRequest,
+                ActionListener.wrap(
+                    response -> listener.onResponse(rolloverRequest.areConditionsMet(response.getConditionStatus()), EmptyInfo.INSTANCE),
+                    listener::onFailure
+                )
+            );
+    }
+
+    /**
+     * Builds a RolloverRequest that captures the various max_* and min_* conditions of this {@link WaitForRolloverReadyStep}.
+     *
+     * To prevent empty indices from rolling over, a `min_docs: 1` condition will be injected if `rolloverOnlyIfHasDocuments` is true
+     * and the request doesn't already have an associated min_docs or min_primary_shard_docs condition.
+     *
+     * @param rolloverTarget the index to rollover
+     * @param masterTimeout the master timeout to use with the request
+     * @param rolloverOnlyIfHasDocuments whether to inject a min_docs 1 condition if there is not already a min_docs
+     *                                   (or min_primary_shard_docs) condition
+     * @return A RolloverRequest suitable for passing to {@code rolloverIndex(...) }.
+     */
+    // visible for testing
+    RolloverRequest createRolloverRequest(String rolloverTarget, TimeValue masterTimeout, boolean rolloverOnlyIfHasDocuments) {
         RolloverRequest rolloverRequest = new RolloverRequest(rolloverTarget, null).masterNodeTimeout(masterTimeout);
         rolloverRequest.dryRun(true);
         if (maxSize != null) {
@@ -228,15 +258,11 @@ public class WaitForRolloverReadyStep extends AsyncWaitStep {
         if (minPrimaryShardDocs != null) {
             rolloverRequest.addMinPrimaryShardDocsCondition(minPrimaryShardDocs);
         }
-        getClient().admin()
-            .indices()
-            .rolloverIndex(
-                rolloverRequest,
-                ActionListener.wrap(
-                    response -> listener.onResponse(rolloverRequest.areConditionsMet(response.getConditionStatus()), EmptyInfo.INSTANCE),
-                    listener::onFailure
-                )
-            );
+
+        if (rolloverOnlyIfHasDocuments && (minDocs == null && minPrimaryShardDocs == null)) {
+            rolloverRequest.addMinIndexDocsCondition(1L);
+        }
+        return rolloverRequest;
     }
 
     ByteSizeValue getMaxSize() {

+ 69 - 108
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStepTests.java

@@ -184,13 +184,54 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
         assertEquals(1, request.indices().length);
         assertEquals(rolloverTarget, request.indices()[0]);
         assertEquals(rolloverTarget, request.getRolloverTarget());
-        assertEquals(expectedConditions.size(), request.getConditions().size());
         assertTrue(request.isDryRun());
+        assertEquals(expectedConditions.size(), request.getConditions().size());
         Set<Object> expectedConditionValues = expectedConditions.stream().map(Condition::value).collect(Collectors.toSet());
         Set<Object> actualConditionValues = request.getConditions().values().stream().map(Condition::value).collect(Collectors.toSet());
         assertEquals(expectedConditionValues, actualConditionValues);
     }
 
+    private static Set<Condition<?>> getExpectedConditions(WaitForRolloverReadyStep step, boolean maybeAddMinDocs) {
+        Set<Condition<?>> expectedConditions = new HashSet<>();
+        if (step.getMaxSize() != null) {
+            expectedConditions.add(new MaxSizeCondition(step.getMaxSize()));
+        }
+        if (step.getMaxPrimaryShardSize() != null) {
+            expectedConditions.add(new MaxPrimaryShardSizeCondition(step.getMaxPrimaryShardSize()));
+        }
+        if (step.getMaxAge() != null) {
+            expectedConditions.add(new MaxAgeCondition(step.getMaxAge()));
+        }
+        if (step.getMaxDocs() != null) {
+            expectedConditions.add(new MaxDocsCondition(step.getMaxDocs()));
+        }
+        if (step.getMaxPrimaryShardDocs() != null) {
+            expectedConditions.add(new MaxPrimaryShardDocsCondition(step.getMaxPrimaryShardDocs()));
+        }
+        if (step.getMinSize() != null) {
+            expectedConditions.add(new MinSizeCondition(step.getMinSize()));
+        }
+        if (step.getMinPrimaryShardSize() != null) {
+            expectedConditions.add(new MinPrimaryShardSizeCondition(step.getMinPrimaryShardSize()));
+        }
+        if (step.getMinAge() != null) {
+            expectedConditions.add(new MinAgeCondition(step.getMinAge()));
+        }
+        if (step.getMinDocs() != null) {
+            expectedConditions.add(new MinDocsCondition(step.getMinDocs()));
+        }
+        if (step.getMinPrimaryShardDocs() != null) {
+            expectedConditions.add(new MinPrimaryShardDocsCondition(step.getMinPrimaryShardDocs()));
+        }
+
+        // if no minimum document condition was specified, then a default min_docs: 1 condition will be injected (if desired)
+        if (maybeAddMinDocs && step.getMinDocs() == null && step.getMinPrimaryShardDocs() == null) {
+            expectedConditions.add(new MinDocsCondition(1L));
+        }
+
+        return expectedConditions;
+    }
+
     public void testEvaluateCondition() {
         String alias = randomAlphaOfLength(5);
         IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(10))
@@ -202,7 +243,7 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
 
         WaitForRolloverReadyStep step = createRandomInstance();
 
-        mockRolloverIndexCall(alias, step);
+        mockRolloverIndexCall(alias, step, true);
 
         SetOnce<Boolean> conditionsMet = new SetOnce<>();
         step.evaluateCondition(Metadata.builder().put(indexMetadata, true).build(), indexMetadata.getIndex(), new AsyncWaitStep.Listener() {
@@ -235,7 +276,7 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
 
         WaitForRolloverReadyStep step = createRandomInstance();
 
-        mockRolloverIndexCall(dataStreamName, step);
+        mockRolloverIndexCall(dataStreamName, step, true);
 
         SetOnce<Boolean> conditionsMet = new SetOnce<>();
         Metadata metadata = Metadata.builder()
@@ -303,45 +344,15 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
         verifyNoMoreInteractions(indicesClient);
     }
 
-    private void mockRolloverIndexCall(String rolloverTarget, WaitForRolloverReadyStep step) {
+    private void mockRolloverIndexCall(String rolloverTarget, WaitForRolloverReadyStep step, boolean conditionResult) {
         Mockito.doAnswer(invocation -> {
             RolloverRequest request = (RolloverRequest) invocation.getArguments()[0];
             @SuppressWarnings("unchecked")
             ActionListener<RolloverResponse> listener = (ActionListener<RolloverResponse>) invocation.getArguments()[1];
-            Set<Condition<?>> expectedConditions = new HashSet<>();
-            if (step.getMaxSize() != null) {
-                expectedConditions.add(new MaxSizeCondition(step.getMaxSize()));
-            }
-            if (step.getMaxPrimaryShardSize() != null) {
-                expectedConditions.add(new MaxPrimaryShardSizeCondition(step.getMaxPrimaryShardSize()));
-            }
-            if (step.getMaxAge() != null) {
-                expectedConditions.add(new MaxAgeCondition(step.getMaxAge()));
-            }
-            if (step.getMaxDocs() != null) {
-                expectedConditions.add(new MaxDocsCondition(step.getMaxDocs()));
-            }
-            if (step.getMaxPrimaryShardDocs() != null) {
-                expectedConditions.add(new MaxPrimaryShardDocsCondition(step.getMaxPrimaryShardDocs()));
-            }
-            if (step.getMinSize() != null) {
-                expectedConditions.add(new MinSizeCondition(step.getMinSize()));
-            }
-            if (step.getMinPrimaryShardSize() != null) {
-                expectedConditions.add(new MinPrimaryShardSizeCondition(step.getMinPrimaryShardSize()));
-            }
-            if (step.getMinAge() != null) {
-                expectedConditions.add(new MinAgeCondition(step.getMinAge()));
-            }
-            if (step.getMinDocs() != null) {
-                expectedConditions.add(new MinDocsCondition(step.getMinDocs()));
-            }
-            if (step.getMinPrimaryShardDocs() != null) {
-                expectedConditions.add(new MinPrimaryShardDocsCondition(step.getMinPrimaryShardDocs()));
-            }
+            Set<Condition<?>> expectedConditions = getExpectedConditions(step, true);
             assertRolloverIndexRequest(request, rolloverTarget, expectedConditions);
             Map<String, Boolean> conditionResults = expectedConditions.stream()
-                .collect(Collectors.toMap(Condition::toString, condition -> true));
+                .collect(Collectors.toMap(Condition::toString, condition -> conditionResult));
             listener.onResponse(new RolloverResponse(null, null, conditionResults, request.isDryRun(), false, false, false));
             return null;
         }).when(indicesClient).rolloverIndex(Mockito.any(), Mockito.any());
@@ -524,47 +535,7 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
             .build();
         WaitForRolloverReadyStep step = createRandomInstance();
 
-        Mockito.doAnswer(invocation -> {
-            RolloverRequest request = (RolloverRequest) invocation.getArguments()[0];
-            @SuppressWarnings("unchecked")
-            ActionListener<RolloverResponse> listener = (ActionListener<RolloverResponse>) invocation.getArguments()[1];
-            Set<Condition<?>> expectedConditions = new HashSet<>();
-            if (step.getMaxSize() != null) {
-                expectedConditions.add(new MaxSizeCondition(step.getMaxSize()));
-            }
-            if (step.getMaxPrimaryShardSize() != null) {
-                expectedConditions.add(new MaxPrimaryShardSizeCondition(step.getMaxPrimaryShardSize()));
-            }
-            if (step.getMaxAge() != null) {
-                expectedConditions.add(new MaxAgeCondition(step.getMaxAge()));
-            }
-            if (step.getMaxDocs() != null) {
-                expectedConditions.add(new MaxDocsCondition(step.getMaxDocs()));
-            }
-            if (step.getMaxPrimaryShardDocs() != null) {
-                expectedConditions.add(new MaxPrimaryShardDocsCondition(step.getMaxPrimaryShardDocs()));
-            }
-            if (step.getMinSize() != null) {
-                expectedConditions.add(new MinSizeCondition(step.getMinSize()));
-            }
-            if (step.getMinPrimaryShardSize() != null) {
-                expectedConditions.add(new MinPrimaryShardSizeCondition(step.getMinPrimaryShardSize()));
-            }
-            if (step.getMinAge() != null) {
-                expectedConditions.add(new MinAgeCondition(step.getMinAge()));
-            }
-            if (step.getMinDocs() != null) {
-                expectedConditions.add(new MinDocsCondition(step.getMinDocs()));
-            }
-            if (step.getMinPrimaryShardDocs() != null) {
-                expectedConditions.add(new MinPrimaryShardDocsCondition(step.getMinPrimaryShardDocs()));
-            }
-            assertRolloverIndexRequest(request, alias, expectedConditions);
-            Map<String, Boolean> conditionResults = expectedConditions.stream()
-                .collect(Collectors.toMap(Condition::toString, condition -> false));
-            listener.onResponse(new RolloverResponse(null, null, conditionResults, request.isDryRun(), false, false, false));
-            return null;
-        }).when(indicesClient).rolloverIndex(Mockito.any(), Mockito.any());
+        mockRolloverIndexCall(alias, step, false);
 
         SetOnce<Boolean> actionCompleted = new SetOnce<>();
         step.evaluateCondition(Metadata.builder().put(indexMetadata, true).build(), indexMetadata.getIndex(), new AsyncWaitStep.Listener() {
@@ -602,37 +573,7 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
             RolloverRequest request = (RolloverRequest) invocation.getArguments()[0];
             @SuppressWarnings("unchecked")
             ActionListener<RolloverResponse> listener = (ActionListener<RolloverResponse>) invocation.getArguments()[1];
-            Set<Condition<?>> expectedConditions = new HashSet<>();
-            if (step.getMaxSize() != null) {
-                expectedConditions.add(new MaxSizeCondition(step.getMaxSize()));
-            }
-            if (step.getMaxPrimaryShardSize() != null) {
-                expectedConditions.add(new MaxPrimaryShardSizeCondition(step.getMaxPrimaryShardSize()));
-            }
-            if (step.getMaxAge() != null) {
-                expectedConditions.add(new MaxAgeCondition(step.getMaxAge()));
-            }
-            if (step.getMaxDocs() != null) {
-                expectedConditions.add(new MaxDocsCondition(step.getMaxDocs()));
-            }
-            if (step.getMaxPrimaryShardDocs() != null) {
-                expectedConditions.add(new MaxPrimaryShardDocsCondition(step.getMaxPrimaryShardDocs()));
-            }
-            if (step.getMinSize() != null) {
-                expectedConditions.add(new MinSizeCondition(step.getMinSize()));
-            }
-            if (step.getMinPrimaryShardSize() != null) {
-                expectedConditions.add(new MinPrimaryShardSizeCondition(step.getMinPrimaryShardSize()));
-            }
-            if (step.getMinAge() != null) {
-                expectedConditions.add(new MinAgeCondition(step.getMinAge()));
-            }
-            if (step.getMinDocs() != null) {
-                expectedConditions.add(new MinDocsCondition(step.getMinDocs()));
-            }
-            if (step.getMinPrimaryShardDocs() != null) {
-                expectedConditions.add(new MinPrimaryShardDocsCondition(step.getMinPrimaryShardDocs()));
-            }
+            Set<Condition<?>> expectedConditions = getExpectedConditions(step, true);
             assertRolloverIndexRequest(request, alias, expectedConditions);
             listener.onFailure(exception);
             return null;
@@ -730,4 +671,24 @@ public class WaitForRolloverReadyStepTests extends AbstractStepTestCase<WaitForR
             )
         );
     }
+
+    public void testCreateRolloverRequestRolloverOnlyIfHasDocuments() {
+        boolean rolloverOnlyIfHasDocuments = randomBoolean();
+
+        WaitForRolloverReadyStep step = createRandomInstance();
+        String rolloverTarget = randomAlphaOfLength(5);
+        TimeValue masterTimeout = TimeValue.parseTimeValue(randomPositiveTimeValue(), "rollover_action_test");
+
+        RolloverRequest request = step.createRolloverRequest(rolloverTarget, masterTimeout, rolloverOnlyIfHasDocuments);
+
+        assertThat(request.getRolloverTarget(), is(rolloverTarget));
+        assertThat(request.masterNodeTimeout(), is(masterTimeout));
+        assertThat(request.isDryRun(), is(true)); // it's always a dry_run
+
+        Set<Condition<?>> expectedConditions = getExpectedConditions(step, rolloverOnlyIfHasDocuments);
+        assertEquals(expectedConditions.size(), request.getConditions().size());
+        Set<Object> expectedConditionValues = expectedConditions.stream().map(Condition::value).collect(Collectors.toSet());
+        Set<Object> actualConditionValues = request.getConditions().values().stream().map(Condition::value).collect(Collectors.toSet());
+        assertEquals(expectedConditionValues, actualConditionValues);
+    }
 }

+ 105 - 1
x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/RolloverActionIT.java

@@ -8,9 +8,12 @@
 package org.elasticsearch.xpack.ilm.actions;
 
 import org.elasticsearch.client.Request;
+import org.elasticsearch.client.WarningFailureException;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.core.CheckedRunnable;
+import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction;
 import org.elasticsearch.test.rest.ESRestTestCase;
@@ -193,6 +196,107 @@ public class RolloverActionIT extends ESRestTestCase {
         }, 30, TimeUnit.SECONDS);
     }
 
+    /**
+     * There are multiple scenarios where we want to set up an empty index, make sure that it *doesn't* roll over, then change something
+     * about the cluster, and verify that now the index does roll over. This is a 'template method' that allows you to provide a runnable
+     * which will accomplish that end. Each invocation of this should live in its own top-level `public void test...` method.
+     */
+    private void templateTestRolloverActionWithEmptyIndex(CheckedRunnable<Exception> allowEmptyIndexToRolloverRunnable) throws Exception {
+        String originalIndex = index + "-000001";
+        String secondIndex = index + "-000002";
+
+        createIndexWithSettings(
+            client(),
+            originalIndex,
+            alias,
+            Settings.builder()
+                .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
+                .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
+                .put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, alias)
+        );
+
+        // create policy
+        createNewSingletonPolicy(
+            client(),
+            policy,
+            "hot",
+            new RolloverAction(null, null, TimeValue.timeValueSeconds(1), null, null, null, null, null, null, null)
+        );
+        // update policy on index
+        updatePolicy(client(), originalIndex, policy);
+
+        // maybe set the controlling setting to explicitly true (rather than just true by default)
+        if (randomBoolean()) {
+            setLifecycleRolloverOnlyIfHasDocumentsSetting(true);
+        }
+
+        // because the index is empty, it doesn't roll over
+        assertBusy(() -> {
+            assertThat(getStepKeyForIndex(client(), originalIndex).getName(), is(WaitForRolloverReadyStep.NAME));
+            assertFalse(indexExists(secondIndex));
+            assertTrue(indexExists(originalIndex));
+        }, 30, TimeUnit.SECONDS);
+
+        // run the passed in runnable that will somehow make it so the index rolls over
+        allowEmptyIndexToRolloverRunnable.run();
+
+        // now the index rolls over as expected
+        assertBusy(() -> {
+            assertThat(getStepKeyForIndex(client(), originalIndex), equalTo(PhaseCompleteStep.finalStep("hot").getKey()));
+            assertTrue(indexExists(secondIndex));
+            assertTrue(indexExists(originalIndex));
+            assertEquals("true", getOnlyIndexSettings(client(), originalIndex).get(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE));
+        }, 30, TimeUnit.SECONDS);
+
+        // reset to null so that the post-test cleanup doesn't fail because it sees a deprecated setting
+        setLifecycleRolloverOnlyIfHasDocumentsSetting(null);
+    }
+
+    public void testRolloverActionWithEmptyIndexThenADocIsIndexed() throws Exception {
+        templateTestRolloverActionWithEmptyIndex(() -> {
+            // index document {"foo": "bar"} to trigger rollover
+            index(client(), index + "-000001", "_id", "foo", "bar");
+        });
+    }
+
+    public void testRolloverActionWithEmptyIndexThenThePolicyIsChanged() throws Exception {
+        templateTestRolloverActionWithEmptyIndex(() -> {
+            // change the policy to permit empty rollovers -- with either min_docs or min_primary_shard_docs set to 0
+            createNewSingletonPolicy(
+                client(),
+                policy,
+                "hot",
+                randomBoolean()
+                    ? new RolloverAction(null, null, TimeValue.timeValueSeconds(1), null, null, null, null, null, 0L, null)
+                    : new RolloverAction(null, null, TimeValue.timeValueSeconds(1), null, null, null, null, null, null, 0L)
+            );
+        });
+    }
+
+    public void testRolloverActionWithEmptyIndexThenTheClusterSettingIsChanged() throws Exception {
+        templateTestRolloverActionWithEmptyIndex(() -> {
+            // change the cluster-wide setting to permit empty rollovers
+            setLifecycleRolloverOnlyIfHasDocumentsSetting(false);
+        });
+    }
+
+    private void setLifecycleRolloverOnlyIfHasDocumentsSetting(@Nullable Boolean value) throws IOException {
+        try {
+            Settings.Builder settings = Settings.builder();
+            if (value != null) {
+                settings.put(LifecycleSettings.LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS, value.booleanValue());
+            } else {
+                settings.putNull(LifecycleSettings.LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS);
+            }
+            updateClusterSettings(settings.build());
+            if (value != null) {
+                fail("expected WarningFailureException from warnings");
+            }
+        } catch (WarningFailureException e) {
+            // expected, this setting is deprecated, so we can get back a warning
+        }
+    }
+
     public void testILMRolloverRetriesOnReadOnlyBlock() throws Exception {
         String firstIndex = index + "-000001";
 
@@ -200,7 +304,7 @@ public class RolloverActionIT extends ESRestTestCase {
             client(),
             policy,
             "hot",
-            new RolloverAction(null, null, TimeValue.timeValueSeconds(1), null, null, null, null, null, null, null)
+            new RolloverAction(null, null, TimeValue.timeValueSeconds(1), null, null, null, null, null, 0L, null)
         );
 
         // create the index as readonly and associate the ILM policy to it

+ 2 - 1
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java

@@ -178,12 +178,13 @@ public class IndexLifecycle extends Plugin implements ActionPlugin, HealthPlugin
         return Arrays.asList(
             LifecycleSettings.LIFECYCLE_POLL_INTERVAL_SETTING,
             LifecycleSettings.LIFECYCLE_NAME_SETTING,
+            LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING,
             LifecycleSettings.LIFECYCLE_ORIGINATION_DATE_SETTING,
             LifecycleSettings.LIFECYCLE_PARSE_ORIGINATION_DATE_SETTING,
-            LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING,
             LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING,
             LifecycleSettings.LIFECYCLE_STEP_MASTER_TIMEOUT_SETTING,
             LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING,
+            LifecycleSettings.LIFECYCLE_ROLLOVER_ONLY_IF_HAS_DOCUMENTS_SETTING,
             RolloverAction.LIFECYCLE_ROLLOVER_ALIAS_SETTING,
             LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING,
             LifecycleSettings.SLM_RETENTION_SCHEDULE_SETTING,