From c7ec0b8431578b3982602c002de4b678c1361b06 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Wed, 7 Aug 2019 08:01:42 -0600 Subject: [PATCH] =?UTF-8?q?Include=20in-progress=20snapshot=20for=20a=20po?= =?UTF-8?q?licy=20with=20get=20SLM=20policy=E2=80=A6=20(#45245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the "in_progress" key to the SLM get policy API, returning a policy that looks like: ```json { "daily-snapshots" : { "version" : 1, "modified_date" : "2019-08-05T18:41:48.778Z", "modified_date_millis" : 1565030508778, "policy" : { "name" : "", "schedule" : "0 30 1 * * ?", "repository" : "repo", "config" : { "indices" : [ "foo-*", "important" ], "ignore_unavailable" : true, "include_global_state" : false }, "retention" : { "expire_after" : "10m" } }, "last_success" : { "snapshot_name" : "production-snap-2019.08.05-oxctmnobqye3luim4uejhg", "time_string" : "2019-08-05T18:42:23.257Z", "time" : 1565030543257 }, "next_execution" : "2019-08-06T01:30:00.000Z", "next_execution_millis" : 1565055000000, "in_progress" : { "name" : "production-snap-2019.08.05-oxctmnobqye3luim4uejhg", "uuid" : "t8Idqt6JQxiZrzp0Vt7z6g", "state" : "STARTED", "start_time" : "2019-08-05T18:42:22.998Z", "start_time_millis" : 1565030542998 } } } ``` These are only visible while the snapshot is being taken (or failed), since it reads from the cluster state rather than from the repository itself. --- .../slm/SnapshotLifecyclePolicyMetadata.java | 101 +++++++++++++++- .../documentation/ILMDocumentationIT.java | 2 + .../core/slm/SnapshotLifecyclePolicy.java | 3 +- .../core/slm/SnapshotLifecyclePolicyItem.java | 109 +++++++++++++++++- .../slm/SnapshotLifecyclePolicyItemTests.java | 37 ++++-- .../xpack/slm/SnapshotLifecycleIT.java | 70 ++++++++++- .../TransportGetSnapshotLifecycleAction.java | 29 ++++- 7 files changed, 332 insertions(+), 19 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/slm/SnapshotLifecyclePolicyMetadata.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/slm/SnapshotLifecyclePolicyMetadata.java index 0c1c7083caa..9b967e8c33b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/slm/SnapshotLifecyclePolicyMetadata.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/slm/SnapshotLifecyclePolicyMetadata.java @@ -21,10 +21,12 @@ package org.elasticsearch.client.slm; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.snapshots.SnapshotId; import java.io.IOException; import java.util.Objects; @@ -39,6 +41,7 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { static final ParseField LAST_FAILURE = new ParseField("last_failure"); static final ParseField NEXT_EXECUTION_MILLIS = new ParseField("next_execution_millis"); static final ParseField NEXT_EXECUTION = new ParseField("next_execution"); + static final ParseField SNAPSHOT_IN_PROGRESS = new ParseField("in_progress"); private final SnapshotLifecyclePolicy policy; private final long version; @@ -48,6 +51,8 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { private final SnapshotInvocationRecord lastSuccess; @Nullable private final SnapshotInvocationRecord lastFailure; + @Nullable + private final SnapshotInProgress snapshotInProgress; @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = @@ -59,8 +64,9 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { SnapshotInvocationRecord lastSuccess = (SnapshotInvocationRecord) a[3]; SnapshotInvocationRecord lastFailure = (SnapshotInvocationRecord) a[4]; long nextExecution = (long) a[5]; + SnapshotInProgress sip = (SnapshotInProgress) a[6]; - return new SnapshotLifecyclePolicyMetadata(policy, version, modifiedDate, lastSuccess, lastFailure, nextExecution); + return new SnapshotLifecyclePolicyMetadata(policy, version, modifiedDate, lastSuccess, lastFailure, nextExecution, sip); }); static { @@ -70,6 +76,7 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SnapshotInvocationRecord::parse, LAST_SUCCESS); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SnapshotInvocationRecord::parse, LAST_FAILURE); PARSER.declareLong(ConstructingObjectParser.constructorArg(), NEXT_EXECUTION_MILLIS); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SnapshotInProgress::parse, SNAPSHOT_IN_PROGRESS); } public static SnapshotLifecyclePolicyMetadata parse(XContentParser parser, String id) { @@ -78,13 +85,15 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { public SnapshotLifecyclePolicyMetadata(SnapshotLifecyclePolicy policy, long version, long modifiedDate, SnapshotInvocationRecord lastSuccess, SnapshotInvocationRecord lastFailure, - long nextExecution) { + long nextExecution, + @Nullable SnapshotInProgress snapshotInProgress) { this.policy = policy; this.version = version; this.modifiedDate = modifiedDate; this.lastSuccess = lastSuccess; this.lastFailure = lastFailure; this.nextExecution = nextExecution; + this.snapshotInProgress = snapshotInProgress; } public SnapshotLifecyclePolicy getPolicy() { @@ -115,6 +124,11 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { return this.nextExecution; } + @Nullable + public SnapshotInProgress getSnapshotInProgress() { + return this.snapshotInProgress; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -128,6 +142,9 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { builder.field(LAST_FAILURE.getPreferredName(), lastFailure); } builder.timeField(NEXT_EXECUTION_MILLIS.getPreferredName(), NEXT_EXECUTION.getPreferredName(), nextExecution); + if (snapshotInProgress != null) { + builder.field(SNAPSHOT_IN_PROGRESS.getPreferredName(), snapshotInProgress); + } builder.endObject(); return builder; } @@ -154,4 +171,84 @@ public class SnapshotLifecyclePolicyMetadata implements ToXContentObject { Objects.equals(nextExecution, other.nextExecution); } + public static class SnapshotInProgress implements ToXContentObject { + private static final ParseField NAME = new ParseField("name"); + private static final ParseField UUID = new ParseField("uuid"); + private static final ParseField STATE = new ParseField("state"); + private static final ParseField START_TIME = new ParseField("start_time_millis"); + private static final ParseField FAILURE = new ParseField("failure"); + + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("snapshot_in_progress", true, a -> { + SnapshotId id = new SnapshotId((String) a[0], (String) a[1]); + String state = (String) a[2]; + long start = (long) a[3]; + String failure = (String) a[4]; + return new SnapshotInProgress(id, state, start, failure); + }); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME); + PARSER.declareString(ConstructingObjectParser.constructorArg(), UUID); + PARSER.declareString(ConstructingObjectParser.constructorArg(), STATE); + PARSER.declareLong(ConstructingObjectParser.constructorArg(), START_TIME); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), FAILURE); + } + + private final SnapshotId snapshotId; + private final String state; + private final long startTime; + private final String failure; + + public SnapshotInProgress(SnapshotId snapshotId, String state, long startTime, @Nullable String failure) { + this.snapshotId = snapshotId; + this.state = state; + this.startTime = startTime; + this.failure = failure; + } + + private static SnapshotInProgress parse(XContentParser parser, String name) { + return PARSER.apply(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(NAME.getPreferredName(), snapshotId.getName()); + builder.field(UUID.getPreferredName(), snapshotId.getUUID()); + builder.field(STATE.getPreferredName(), state); + builder.timeField(START_TIME.getPreferredName(), "start_time", startTime); + if (failure != null) { + builder.field(FAILURE.getPreferredName(), failure); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(snapshotId, state, startTime, failure); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (obj.getClass() != getClass()) { + return false; + } + SnapshotInProgress other = (SnapshotInProgress) obj; + return Objects.equals(snapshotId, other.snapshotId) && + Objects.equals(state, other.state) && + startTime == other.startTime && + Objects.equals(failure, other.failure); + } + + @Override + public String toString() { + return Strings.toString(this); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java index 3b3059c29bd..2d876905dfb 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ILMDocumentationIT.java @@ -854,6 +854,8 @@ public class ILMDocumentationIT extends ESRestHighLevelClientTestCase { long nextPolicyExecutionDate = policyMeta.getNextExecution(); SnapshotInvocationRecord lastSuccess = policyMeta.getLastSuccess(); SnapshotInvocationRecord lastFailure = policyMeta.getLastFailure(); + SnapshotLifecyclePolicyMetadata.SnapshotInProgress inProgress = + policyMeta.getSnapshotInProgress(); SnapshotLifecyclePolicy retrievedPolicy = policyMeta.getPolicy(); // <2> String id = retrievedPolicy.getId(); String snapshotNameFormat = retrievedPolicy.getName(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java index dfd2d5697a2..24e319c9b56 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java @@ -47,6 +47,8 @@ import static org.elasticsearch.cluster.metadata.MetaDataCreateIndexService.MAX_ public class SnapshotLifecyclePolicy extends AbstractDiffable implements Writeable, Diffable, ToXContentObject { + public static final String POLICY_ID_METADATA_FIELD = "policy"; + private final String id; private final String name; private final String schedule; @@ -59,7 +61,6 @@ public class SnapshotLifecyclePolicy extends AbstractDiffable { + public static SnapshotLifecyclePolicyItem.SnapshotInProgress randomSnapshotInProgress() { + return rarely() ? null : new SnapshotLifecyclePolicyItem.SnapshotInProgress( + new SnapshotId("name-" + randomAlphaOfLength(3), "uuid-" + randomAlphaOfLength(3)), + randomFrom(SnapshotsInProgress.State.values()), + randomNonNegativeLong(), + randomBoolean() ? null : "failure!"); + } + @Override protected SnapshotLifecyclePolicyItem createTestInstance() { - return new SnapshotLifecyclePolicyItem(createRandomPolicyMetadata(randomAlphaOfLengthBetween(5, 10))); + return new SnapshotLifecyclePolicyItem(createRandomPolicyMetadata(randomAlphaOfLengthBetween(5, 10)), randomSnapshotInProgress()); } @Override protected SnapshotLifecyclePolicyItem mutateInstance(SnapshotLifecyclePolicyItem instance) { - switch (between(0, 4)) { + switch (between(0, 5)) { case 0: String newPolicyId = randomValueOtherThan(instance.getPolicy().getId(), () -> randomAlphaOfLengthBetween(5, 10)); return new SnapshotLifecyclePolicyItem(createRandomPolicy(newPolicyId), instance.getVersion(), instance.getModifiedDate(), instance.getLastSuccess(), - instance.getLastFailure()); + instance.getLastFailure(), + instance.getSnapshotInProgress()); case 1: return new SnapshotLifecyclePolicyItem(instance.getPolicy(), randomValueOtherThan(instance.getVersion(), ESTestCase::randomNonNegativeLong), instance.getModifiedDate(), instance.getLastSuccess(), - instance.getLastFailure()); + instance.getLastFailure(), + instance.getSnapshotInProgress()); case 2: return new SnapshotLifecyclePolicyItem(instance.getPolicy(), instance.getVersion(), randomValueOtherThan(instance.getModifiedDate(), ESTestCase::randomNonNegativeLong), instance.getLastSuccess(), - instance.getLastFailure()); + instance.getLastFailure(), + instance.getSnapshotInProgress()); case 3: return new SnapshotLifecyclePolicyItem(instance.getPolicy(), instance.getVersion(), instance.getModifiedDate(), randomValueOtherThan(instance.getLastSuccess(), SnapshotInvocationRecordTests::randomSnapshotInvocationRecord), - instance.getLastFailure()); + instance.getLastFailure(), + instance.getSnapshotInProgress()); case 4: return new SnapshotLifecyclePolicyItem(instance.getPolicy(), instance.getVersion(), instance.getModifiedDate(), instance.getLastSuccess(), randomValueOtherThan(instance.getLastFailure(), - SnapshotInvocationRecordTests::randomSnapshotInvocationRecord)); + SnapshotInvocationRecordTests::randomSnapshotInvocationRecord), + instance.getSnapshotInProgress()); + case 5: + return new SnapshotLifecyclePolicyItem(instance.getPolicy(), + instance.getVersion(), + instance.getModifiedDate(), + instance.getLastSuccess(), + instance.getLastFailure(), + randomValueOtherThan(instance.getSnapshotInProgress(), + SnapshotLifecyclePolicyItemTests::randomSnapshotInProgress)); default: throw new AssertionError("failure, got illegal switch case"); } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleIT.java index a2301694083..f03281e6669 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/slm/SnapshotLifecycleIT.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.containsString; @@ -168,7 +169,6 @@ public class SnapshotLifecycleIT extends ESRestTestCase { final String policyName = "test-policy"; final String repoId = "my-repo"; int docCount = randomIntBetween(10, 50); - List indexReqs = new ArrayList<>(); for (int i = 0; i < docCount; i++) { index(client(), indexName, "" + i, "foo", "bar"); } @@ -218,6 +218,68 @@ public class SnapshotLifecycleIT extends ESRestTestCase { }); } + @SuppressWarnings("unchecked") + public void testSnapshotInProgress() throws Exception { + final String indexName = "test"; + final String policyName = "test-policy"; + final String repoId = "my-repo"; + int docCount = 20; + for (int i = 0; i < docCount; i++) { + index(client(), indexName, "" + i, "foo", "bar"); + } + + // Create a snapshot repo + inializeRepo(repoId, 1); + + createSnapshotPolicy(policyName, "snap", "1 2 3 4 5 ?", repoId, indexName, true); + + Response executeRepsonse = client().performRequest(new Request("PUT", "/_slm/policy/" + policyName + "/_execute")); + + try (XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, EntityUtils.toByteArray(executeRepsonse.getEntity()))) { + final String snapshotName = parser.mapStrings().get("snapshot_name"); + + // Check that the executed snapshot shows up in the SLM output + assertBusy(() -> { + try { + Response response = client().performRequest(new Request("GET", "/_slm/policy" + (randomBoolean() ? "" : "?human"))); + Map policyResponseMap; + try (InputStream content = response.getEntity().getContent()) { + policyResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), content, true); + } + assertThat(policyResponseMap.size(), greaterThan(0)); + Optional> inProgress = Optional.ofNullable((Map) policyResponseMap.get(policyName)) + .map(policy -> (Map) policy.get("in_progress")); + + if (inProgress.isPresent()) { + Map inProgressMap = inProgress.get(); + assertThat(inProgressMap.get("name"), equalTo(snapshotName)); + assertNotNull(inProgressMap.get("uuid")); + assertThat(inProgressMap.get("state"), equalTo("STARTED")); + assertThat((long) inProgressMap.get("start_time_millis"), greaterThan(0L)); + assertNull(inProgressMap.get("failure")); + } else { + fail("expected in_progress to contain a running snapshot, but the response was " + policyResponseMap); + } + } catch (ResponseException e) { + fail("expected policy to exist but it does not: " + EntityUtils.toString(e.getResponse().getEntity())); + } + }); + + // Cancel the snapshot since it is not going to complete quickly + assertOK(client().performRequest(new Request("DELETE", "/_snapshot/" + repoId + "/" + snapshotName))); + } + + Request delReq = new Request("DELETE", "/_slm/policy/" + policyName); + assertOK(client().performRequest(delReq)); + + // It's possible there could have been a snapshot in progress when the + // policy is deleted, so wait for it to be finished + assertBusy(() -> { + assertThat(wipeSnapshots().size(), equalTo(0)); + }); + } + @SuppressWarnings("unchecked") private static Map extractMetadata(Map snapshotResponseMap, String snapshotPrefix) { List> snapshots = ((List>) snapshotResponseMap.get("snapshots")); @@ -298,6 +360,10 @@ public class SnapshotLifecycleIT extends ESRestTestCase { } private void inializeRepo(String repoName) throws IOException { + inializeRepo(repoName, 256); + } + + private void inializeRepo(String repoName, int maxBytesPerSecond) throws IOException { Request request = new Request("PUT", "/_snapshot/" + repoName); request.setJsonEntity(Strings .toString(JsonXContent.contentBuilder() @@ -306,7 +372,7 @@ public class SnapshotLifecycleIT extends ESRestTestCase { .startObject("settings") .field("compress", randomBoolean()) .field("location", System.getProperty("tests.path.repo")) - .field("max_snapshot_bytes_per_sec", "256b") + .field("max_snapshot_bytes_per_sec", maxBytesPerSecond + "b") .endObject() .endObject())); assertOK(client().performRequest(request)); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSnapshotLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSnapshotLifecycleAction.java index c126ef19d2d..98331b9f639 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSnapshotLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSnapshotLifecycleAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.master.TransportMasterNodeAction; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; @@ -21,14 +22,17 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; +import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyItem; import org.elasticsearch.xpack.core.slm.action.GetSnapshotLifecycleAction; import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -61,10 +65,27 @@ public class TransportGetSnapshotLifecycleAction extends if (snapMeta == null) { listener.onResponse(new GetSnapshotLifecycleAction.Response(Collections.emptyList())); } else { + final Map inProgress; + SnapshotsInProgress sip = state.custom(SnapshotsInProgress.TYPE); + if (sip == null) { + inProgress = Collections.emptyMap(); + } else { + inProgress = new HashMap<>(); + for (SnapshotsInProgress.Entry entry : sip.entries()) { + Map meta = entry.userMetadata(); + if (meta == null || + meta.get(SnapshotLifecyclePolicy.POLICY_ID_METADATA_FIELD) == null || + (meta.get(SnapshotLifecyclePolicy.POLICY_ID_METADATA_FIELD) instanceof String == false)) { + continue; + } + + String policyId = (String) meta.get(SnapshotLifecyclePolicy.POLICY_ID_METADATA_FIELD); + inProgress.put(policyId, SnapshotLifecyclePolicyItem.SnapshotInProgress.fromEntry(entry)); + } + } + final Set ids = new HashSet<>(Arrays.asList(request.getLifecycleIds())); - List lifecycles = snapMeta.getSnapshotConfigurations() - .values() - .stream() + List lifecycles = snapMeta.getSnapshotConfigurations().values().stream() .filter(meta -> { if (ids.isEmpty()) { return true; @@ -72,7 +93,7 @@ public class TransportGetSnapshotLifecycleAction extends return ids.contains(meta.getPolicy().getId()); } }) - .map(SnapshotLifecyclePolicyItem::new) + .map(policyMeta -> new SnapshotLifecyclePolicyItem(policyMeta, inProgress.get(policyMeta.getPolicy().getId()))) .collect(Collectors.toList()); listener.onResponse(new GetSnapshotLifecycleAction.Response(lifecycles)); }