diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 60dafd03f9c..126a9c7d4b4 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -43,6 +43,7 @@ import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest; import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequest; @@ -963,6 +964,20 @@ final class RequestConverters { return request; } + static Request snapshotsStatus(SnapshotsStatusRequest snapshotsStatusRequest) { + String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot") + .addPathPart(snapshotsStatusRequest.repository()) + .addCommaSeparatedPathParts(snapshotsStatusRequest.snapshots()) + .addPathPartAsIs("_status") + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + + Params parameters = new Params(request); + parameters.withMasterTimeout(snapshotsStatusRequest.masterNodeTimeout()); + parameters.withIgnoreUnavailable(snapshotsStatusRequest.ignoreUnavailable()); + return request; + } + static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) { String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot") .addPathPart(deleteSnapshotRequest.repository()) @@ -1262,7 +1277,7 @@ final class RequestConverters { } Params withIndicesOptions(IndicesOptions indicesOptions) { - putParam("ignore_unavailable", Boolean.toString(indicesOptions.ignoreUnavailable())); + withIgnoreUnavailable(indicesOptions.ignoreUnavailable()); putParam("allow_no_indices", Boolean.toString(indicesOptions.allowNoIndices())); String expandWildcards; if (indicesOptions.expandWildcardsOpen() == false && indicesOptions.expandWildcardsClosed() == false) { @@ -1281,6 +1296,12 @@ final class RequestConverters { return this; } + Params withIgnoreUnavailable(boolean ignoreUnavailable) { + // Always explicitly place the ignore_unavailable value. + putParam("ignore_unavailable", Boolean.toString(ignoreUnavailable)); + return this; + } + Params withHuman(boolean human) { if (human) { putParam("human", Boolean.toString(human)); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java index fa147a338de..bc0bbe95488 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java @@ -30,6 +30,8 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyReposito import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; @@ -221,6 +223,35 @@ public final class SnapshotClient { GetSnapshotsResponse::fromXContent, listener, emptySet()); } + /** + * Gets the status of requested snapshots. + * See Snapshot and Restore + * API on elastic.co + * @param snapshotsStatusRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public SnapshotsStatusResponse status(SnapshotsStatusRequest snapshotsStatusRequest, RequestOptions options) + throws IOException { + return restHighLevelClient.performRequestAndParseEntity(snapshotsStatusRequest, RequestConverters::snapshotsStatus, options, + SnapshotsStatusResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously gets the status of requested snapshots. + * See Snapshot and Restore + * API on elastic.co + * @param snapshotsStatusRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void statusAsync(SnapshotsStatusRequest snapshotsStatusRequest, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(snapshotsStatusRequest, RequestConverters::snapshotsStatus, options, + SnapshotsStatusResponse::fromXContent, listener, emptySet()); + } + /** * Deletes a snapshot. * See Snapshot and Restore diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 255554be676..fb4e3b22712 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -43,6 +43,7 @@ import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotReq import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; @@ -175,6 +176,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXC import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -2171,6 +2173,29 @@ public class RequestConvertersTests extends ESTestCase { assertNull(request.getEntity()); } + public void testSnapshotsStatus() { + Map expectedParams = new HashMap<>(); + String repository = randomIndicesNames(1, 1)[0]; + String[] snapshots = randomIndicesNames(1, 5); + StringBuilder snapshotNames = new StringBuilder(snapshots[0]); + for (int idx = 1; idx < snapshots.length; idx++) { + snapshotNames.append(",").append(snapshots[idx]); + } + boolean ignoreUnavailable = randomBoolean(); + String endpoint = "/_snapshot/" + repository + "/" + snapshotNames.toString() + "/_status"; + + SnapshotsStatusRequest snapshotsStatusRequest = new SnapshotsStatusRequest(repository, snapshots); + setRandomMasterTimeout(snapshotsStatusRequest, expectedParams); + snapshotsStatusRequest.ignoreUnavailable(ignoreUnavailable); + expectedParams.put("ignore_unavailable", Boolean.toString(ignoreUnavailable)); + + Request request = RequestConverters.snapshotsStatus(snapshotsStatusRequest); + assertThat(request.getEndpoint(), equalTo(endpoint)); + assertThat(request.getMethod(), equalTo(HttpGet.METHOD_NAME)); + assertThat(request.getParameters(), equalTo(expectedParams)); + assertThat(request.getEntity(), is(nullValue())); + } + public void testDeleteSnapshot() { Map expectedParams = new HashMap<>(); String repository = randomIndicesNames(1, 1)[0]; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java index 7ec2ee80f04..45f9b5bbb0b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java @@ -28,6 +28,9 @@ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryResponse; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -43,6 +46,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; public class SnapshotIT extends ESRestHighLevelClientTestCase { @@ -173,6 +177,34 @@ public class SnapshotIT extends ESRestHighLevelClientTestCase { contains("test_snapshot1", "test_snapshot2")); } + public void testSnapshotsStatus() throws IOException { + String testRepository = "test"; + String testSnapshot = "snapshot"; + String testIndex = "test_index"; + + PutRepositoryResponse putRepositoryResponse = createTestRepository(testRepository, FsRepository.TYPE, "{\"location\": \".\"}"); + assertTrue(putRepositoryResponse.isAcknowledged()); + + createIndex(testIndex, Settings.EMPTY); + + CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(testRepository, testSnapshot); + createSnapshotRequest.indices(testIndex); + createSnapshotRequest.waitForCompletion(true); + CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest); + // check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead. + assertEquals(RestStatus.OK, createSnapshotResponse.status()); + + SnapshotsStatusRequest request = new SnapshotsStatusRequest(); + request.repository(testRepository); + request.snapshots(new String[]{testSnapshot}); + SnapshotsStatusResponse response = execute(request, highLevelClient().snapshot()::status, + highLevelClient().snapshot()::statusAsync); + assertThat(response.getSnapshots().size(), equalTo(1)); + assertThat(response.getSnapshots().get(0).getSnapshot().getRepository(), equalTo(testRepository)); + assertThat(response.getSnapshots().get(0).getSnapshot().getSnapshotId().getName(), equalTo(testSnapshot)); + assertThat(response.getSnapshots().get(0).getIndices().containsKey(testIndex), is(true)); + } + public void testDeleteSnapshot() throws IOException { String repository = "test_repository"; String snapshot = "test_snapshot"; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java index 48d01963e23..403ebc7d774 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java @@ -37,11 +37,16 @@ import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotStats; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotStatus; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; +import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.metadata.RepositoryMetaData; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -84,8 +89,8 @@ import static org.hamcrest.Matchers.equalTo; public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase { private static final String repositoryName = "test_repository"; - private static final String snapshotName = "test_snapshot"; + private static final String indexName = "test_index"; public void testSnapshotCreateRepository() throws IOException { RestHighLevelClient client = highLevelClient(); @@ -466,6 +471,7 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase RestHighLevelClient client = highLevelClient(); createTestRepositories(); + createTestIndex(); createTestSnapshots(); // tag::get-snapshots-request @@ -543,10 +549,84 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase } } + public void testSnapshotSnapshotsStatus() throws IOException { + RestHighLevelClient client = highLevelClient(); + createTestRepositories(); + createTestIndex(); + createTestSnapshots(); + + // tag::snapshots-status-request + SnapshotsStatusRequest request = new SnapshotsStatusRequest(); + // end::snapshots-status-request + + // tag::snapshots-status-request-repository + request.repository(repositoryName); // <1> + // end::snapshots-status-request-repository + // tag::snapshots-status-request-snapshots + String [] snapshots = new String[] {snapshotName}; + request.snapshots(snapshots); // <1> + // end::snapshots-status-request-snapshots + // tag::snapshots-status-request-ignoreUnavailable + request.ignoreUnavailable(true); // <1> + // end::snapshots-status-request-ignoreUnavailable + // tag::snapshots-status-request-masterTimeout + request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1> + request.masterNodeTimeout("1m"); // <2> + // end::snapshots-status-request-masterTimeout + + // tag::snapshots-status-execute + SnapshotsStatusResponse response = client.snapshot().status(request, RequestOptions.DEFAULT); + // end::snapshots-status-execute + + // tag::snapshots-status-response + List snapshotStatusesResponse = response.getSnapshots(); + SnapshotStatus snapshotStatus = snapshotStatusesResponse.get(0); // <1> + SnapshotsInProgress.State snapshotState = snapshotStatus.getState(); // <2> + SnapshotStats shardStats = snapshotStatus.getIndices().get(indexName).getShards().get(0).getStats(); // <3> + // end::snapshots-status-response + assertThat(snapshotStatusesResponse.size(), equalTo(1)); + assertThat(snapshotStatusesResponse.get(0).getSnapshot().getRepository(), equalTo(SnapshotClientDocumentationIT.repositoryName)); + assertThat(snapshotStatusesResponse.get(0).getSnapshot().getSnapshotId().getName(), equalTo(snapshotName)); + assertThat(snapshotState.completed(), equalTo(true)); + } + + public void testSnapshotSnapshotsStatusAsync() throws InterruptedException { + RestHighLevelClient client = highLevelClient(); + { + SnapshotsStatusRequest request = new SnapshotsStatusRequest(); + + // tag::snapshots-status-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(SnapshotsStatusResponse snapshotsStatusResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::snapshots-status-execute-listener + + // Replace the empty listener with a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::snapshots-status-execute-async + client.snapshot().statusAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::snapshots-status-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testSnapshotDeleteSnapshot() throws IOException { RestHighLevelClient client = highLevelClient(); createTestRepositories(); + createTestIndex(); createTestSnapshots(); // tag::delete-snapshot-request @@ -608,9 +688,14 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase assertTrue(highLevelClient().snapshot().createRepository(request, RequestOptions.DEFAULT).isAcknowledged()); } + private void createTestIndex() throws IOException { + createIndex(indexName, Settings.EMPTY); + } + private void createTestSnapshots() throws IOException { Request createSnapshot = new Request("put", String.format(Locale.ROOT, "_snapshot/%s/%s", repositoryName, snapshotName)); createSnapshot.addParameter("wait_for_completion", "true"); + createSnapshot.setJsonEntity("{\"indices\":\"" + indexName + "\"}"); Response response = highLevelClient().getLowLevelClient().performRequest(createSnapshot); // check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead. assertEquals(200, response.getStatusLine().getStatusCode()); diff --git a/docs/java-rest/high-level/snapshot/snapshots_status.asciidoc b/docs/java-rest/high-level/snapshot/snapshots_status.asciidoc new file mode 100644 index 00000000000..8f91d774f4e --- /dev/null +++ b/docs/java-rest/high-level/snapshot/snapshots_status.asciidoc @@ -0,0 +1,97 @@ +[[java-rest-high-snapshot-snapshots-status]] +=== Snapshots Status API + +The Snapshots Status API allows to retrieve detailed information about snapshots in progress. + +[[java-rest-high-snapshot-snapshots-status-request]] +==== Snapshots Status Request + +A `SnapshotsStatusRequest`: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-request] +-------------------------------------------------- + +==== Required Arguments +The following arguments must be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-request-repository] +-------------------------------------------------- +<1> Sets the repository to check for snapshot statuses + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-request-snapshots] +-------------------------------------------------- +<1> The list of snapshot names to check the status of + +==== Optional Arguments +The following arguments can optionally be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-request-ignoreUnavailable] +-------------------------------------------------- +<1> The command will fail if some of the snapshots are unavailable. The `ignore_unavailable` flag +set to true will return all snapshots that are currently available. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-request-masterTimeout] +-------------------------------------------------- +<1> Timeout to connect to the master node as a `TimeValue` +<2> Timeout to connect to the master node as a `String` + +[[java-rest-high-snapshot-snapshots-status-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-execute] +-------------------------------------------------- + +[[java-rest-high-snapshot-snapshots-status-async]] +==== Asynchronous Execution + +The asynchronous execution of retrieving snapshot statuses requires both the +`SnapshotsStatusRequest` instance and an `ActionListener` instance to be +passed to the asynchronous method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-execute-async] +-------------------------------------------------- +<1> The `SnapshotsStatusRequest` to execute and the `ActionListener` +to use when the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `SnapshotsStatusResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument +<2> Called in case of a failure. The raised exception is provided as an argument + +[[java-rest-high-snapshot-snapshots-status-response]] +==== Snapshots Status Response + +The returned `SnapshotsStatusResponse` allows to retrieve information about the +executed operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[snapshots-status-response] +-------------------------------------------------- +<1> Request contains a list of snapshot statuses +<2> Each status contains information about the snapshot +<3> Example of reading snapshot statistics about a specific index and shard diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index e69f53eb4ba..cf38040e865 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -154,6 +154,7 @@ The Java High Level REST Client supports the following Snapshot APIs: * <> * <> * <> +* <> * <> include::snapshot/get_repository.asciidoc[] @@ -162,6 +163,7 @@ include::snapshot/delete_repository.asciidoc[] include::snapshot/verify_repository.asciidoc[] include::snapshot/create_snapshot.asciidoc[] include::snapshot/get_snapshots.asciidoc[] +include::snapshot/snapshots_status.asciidoc[] include::snapshot/delete_snapshot.asciidoc[] == Tasks APIs diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatus.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatus.java index 39abd8613ca..834e238e4a0 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatus.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatus.java @@ -19,16 +19,27 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.support.broadcast.BroadcastShardResponse; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; import java.io.IOException; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + public class SnapshotIndexShardStatus extends BroadcastShardResponse implements ToXContentFragment { private SnapshotIndexShardStage stage = SnapshotIndexShardStage.INIT; @@ -80,6 +91,14 @@ public class SnapshotIndexShardStatus extends BroadcastShardResponse implements this.nodeId = nodeId; } + SnapshotIndexShardStatus(ShardId shardId, SnapshotIndexShardStage stage, SnapshotStats stats, String nodeId, String failure) { + super(shardId); + this.stage = stage; + this.stats = stats; + this.nodeId = nodeId; + this.failure = failure; + } + /** * Returns snapshot stage */ @@ -143,7 +162,7 @@ public class SnapshotIndexShardStatus extends BroadcastShardResponse implements public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(Integer.toString(getShardId().getId())); builder.field(Fields.STAGE, getStage()); - stats.toXContent(builder, params); + builder.field(SnapshotStats.Fields.STATS, stats, params); if (getNodeId() != null) { builder.field(Fields.NODE, getNodeId()); } @@ -153,4 +172,72 @@ public class SnapshotIndexShardStatus extends BroadcastShardResponse implements builder.endObject(); return builder; } + + static final ObjectParser.NamedObjectParser PARSER; + static { + ConstructingObjectParser innerParser = new ConstructingObjectParser<>( + "snapshot_index_shard_status", true, + (Object[] parsedObjects, ShardId shard) -> { + int i = 0; + String rawStage = (String) parsedObjects[i++]; + String nodeId = (String) parsedObjects[i++]; + String failure = (String) parsedObjects[i++]; + SnapshotStats stats = (SnapshotStats) parsedObjects[i]; + + SnapshotIndexShardStage stage; + try { + stage = SnapshotIndexShardStage.valueOf(rawStage); + } catch (IllegalArgumentException iae) { + throw new ElasticsearchParseException( + "failed to parse snapshot index shard status [{}][{}], unknonwn stage [{}]", + shard.getIndex().getName(), shard.getId(), rawStage); + } + return new SnapshotIndexShardStatus(shard, stage, stats, nodeId, failure); + } + ); + innerParser.declareString(constructorArg(), new ParseField(Fields.STAGE)); + innerParser.declareString(optionalConstructorArg(), new ParseField(Fields.NODE)); + innerParser.declareString(optionalConstructorArg(), new ParseField(Fields.REASON)); + innerParser.declareObject(constructorArg(), (p, c) -> SnapshotStats.fromXContent(p), new ParseField(SnapshotStats.Fields.STATS)); + PARSER = (p, indexId, shardName) -> { + // Combine the index name in the context with the shard name passed in for the named object parser + // into a ShardId to pass as context for the inner parser. + int shard; + try { + shard = Integer.parseInt(shardName); + } catch (NumberFormatException nfe) { + throw new ElasticsearchParseException( + "failed to parse snapshot index shard status [{}], expected numeric shard id but got [{}]", indexId, shardName); + } + ShardId shardId = new ShardId(new Index(indexId, IndexMetaData.INDEX_UUID_NA_VALUE), shard); + return innerParser.parse(p, shardId); + }; + } + + public static SnapshotIndexShardStatus fromXContent(XContentParser parser, String indexId) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); + return PARSER.parse(parser, indexId, parser.currentName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotIndexShardStatus that = (SnapshotIndexShardStatus) o; + + if (stage != that.stage) return false; + if (stats != null ? !stats.equals(that.stats) : that.stats != null) return false; + if (nodeId != null ? !nodeId.equals(that.nodeId) : that.nodeId != null) return false; + return failure != null ? failure.equals(that.failure) : that.failure == null; + } + + @Override + public int hashCode() { + int result = stage != null ? stage.hashCode() : 0; + result = 31 * result + (stats != null ? stats.hashCode() : 0); + result = 31 * result + (nodeId != null ? nodeId.hashCode() : 0); + result = 31 * result + (failure != null ? failure.hashCode() : 0); + return result; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatus.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatus.java index 1605e41dc61..ba858495980 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatus.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatus.java @@ -19,17 +19,24 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; -import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; +import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; /** * Represents snapshot status of all shards in the index @@ -57,6 +64,14 @@ public class SnapshotIndexStatus implements Iterable, this.indexShards = unmodifiableMap(indexShards); } + public SnapshotIndexStatus(String index, Map indexShards, SnapshotShardsStats shardsStats, + SnapshotStats stats) { + this.index = index; + this.indexShards = indexShards; + this.shardsStats = shardsStats; + this.stats = stats; + } + /** * Returns the index name */ @@ -97,8 +112,8 @@ public class SnapshotIndexStatus implements Iterable, @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(getIndex()); - shardsStats.toXContent(builder, params); - stats.toXContent(builder, params); + builder.field(SnapshotShardsStats.Fields.SHARDS_STATS, shardsStats, params); + builder.field(SnapshotStats.Fields.STATS, stats, params); builder.startObject(Fields.SHARDS); for (SnapshotIndexShardStatus shard : indexShards.values()) { shard.toXContent(builder, params); @@ -107,4 +122,61 @@ public class SnapshotIndexStatus implements Iterable, builder.endObject(); return builder; } + + static final ObjectParser.NamedObjectParser PARSER; + static { + ConstructingObjectParser innerParser = new ConstructingObjectParser<>( + "snapshot_index_status", true, + (Object[] parsedObjects, String index) -> { + int i = 0; + SnapshotShardsStats shardsStats = ((SnapshotShardsStats) parsedObjects[i++]); + SnapshotStats stats = ((SnapshotStats) parsedObjects[i++]); + @SuppressWarnings("unchecked") List shardStatuses = + (List) parsedObjects[i]; + + final Map indexShards; + if (shardStatuses == null || shardStatuses.isEmpty()) { + indexShards = emptyMap(); + } else { + indexShards = new HashMap<>(shardStatuses.size()); + for (SnapshotIndexShardStatus shardStatus : shardStatuses) { + indexShards.put(shardStatus.getShardId().getId(), shardStatus); + } + } + return new SnapshotIndexStatus(index, indexShards, shardsStats, stats); + }); + innerParser.declareObject(constructorArg(), (p, c) -> SnapshotShardsStats.PARSER.apply(p, null), + new ParseField(SnapshotShardsStats.Fields.SHARDS_STATS)); + innerParser.declareObject(constructorArg(), (p, c) -> SnapshotStats.fromXContent(p), + new ParseField(SnapshotStats.Fields.STATS)); + innerParser.declareNamedObjects(constructorArg(), SnapshotIndexShardStatus.PARSER, new ParseField(Fields.SHARDS)); + PARSER = ((p, c, name) -> innerParser.apply(p, name)); + } + + public static SnapshotIndexStatus fromXContent(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); + return PARSER.parse(parser, null, parser.currentName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotIndexStatus that = (SnapshotIndexStatus) o; + + if (index != null ? !index.equals(that.index) : that.index != null) return false; + if (indexShards != null ? !indexShards.equals(that.indexShards) : that.indexShards != null) return false; + if (shardsStats != null ? !shardsStats.equals(that.shardsStats) : that.shardsStats != null) return false; + return stats != null ? stats.equals(that.stats) : that.stats == null; + } + + @Override + public int hashCode() { + int result = index != null ? index.hashCode() : 0; + result = 31 * result + (indexShards != null ? indexShards.hashCode() : 0); + result = 31 * result + (shardsStats != null ? shardsStats.hashCode() : 0); + result = 31 * result + (stats != null ? stats.hashCode() : 0); + return result; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStats.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStats.java index c74dd5af1ee..c0ac432292d 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStats.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStats.java @@ -19,17 +19,22 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Collection; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + /** * Status of a snapshot shards */ -public class SnapshotShardsStats implements ToXContentFragment { +public class SnapshotShardsStats implements ToXContentObject { private int initializingShards; private int startedShards; @@ -63,6 +68,16 @@ public class SnapshotShardsStats implements ToXContentFragment { } } + public SnapshotShardsStats(int initializingShards, int startedShards, int finalizingShards, int doneShards, int failedShards, + int totalShards) { + this.initializingShards = initializingShards; + this.startedShards = startedShards; + this.finalizingShards = finalizingShards; + this.doneShards = doneShards; + this.failedShards = failedShards; + this.totalShards = totalShards; + } + /** * Number of shards with the snapshot in the initializing stage */ @@ -117,15 +132,68 @@ public class SnapshotShardsStats implements ToXContentFragment { @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(Fields.SHARDS_STATS); - builder.field(Fields.INITIALIZING, getInitializingShards()); - builder.field(Fields.STARTED, getStartedShards()); - builder.field(Fields.FINALIZING, getFinalizingShards()); - builder.field(Fields.DONE, getDoneShards()); - builder.field(Fields.FAILED, getFailedShards()); - builder.field(Fields.TOTAL, getTotalShards()); + builder.startObject(); + { + builder.field(Fields.INITIALIZING, getInitializingShards()); + builder.field(Fields.STARTED, getStartedShards()); + builder.field(Fields.FINALIZING, getFinalizingShards()); + builder.field(Fields.DONE, getDoneShards()); + builder.field(Fields.FAILED, getFailedShards()); + builder.field(Fields.TOTAL, getTotalShards()); + } builder.endObject(); return builder; } + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + Fields.SHARDS_STATS, true, + (Object[] parsedObjects) -> { + int i = 0; + int initializingShards = (int) parsedObjects[i++]; + int startedShards = (int) parsedObjects[i++]; + int finalizingShards = (int) parsedObjects[i++]; + int doneShards = (int) parsedObjects[i++]; + int failedShards = (int) parsedObjects[i++]; + int totalShards = (int) parsedObjects[i]; + return new SnapshotShardsStats(initializingShards, startedShards, finalizingShards, doneShards, failedShards, totalShards); + } + ); + static { + PARSER.declareInt(constructorArg(), new ParseField(Fields.INITIALIZING)); + PARSER.declareInt(constructorArg(), new ParseField(Fields.STARTED)); + PARSER.declareInt(constructorArg(), new ParseField(Fields.FINALIZING)); + PARSER.declareInt(constructorArg(), new ParseField(Fields.DONE)); + PARSER.declareInt(constructorArg(), new ParseField(Fields.FAILED)); + PARSER.declareInt(constructorArg(), new ParseField(Fields.TOTAL)); + } + + public static SnapshotShardsStats fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotShardsStats that = (SnapshotShardsStats) o; + + if (initializingShards != that.initializingShards) return false; + if (startedShards != that.startedShards) return false; + if (finalizingShards != that.finalizingShards) return false; + if (doneShards != that.doneShards) return false; + if (failedShards != that.failedShards) return false; + return totalShards == that.totalShards; + } + + @Override + public int hashCode() { + int result = initializingShards; + result = 31 * result + startedShards; + result = 31 * result + finalizingShards; + result = 31 * result + doneShards; + result = 31 * result + failedShards; + result = 31 * result + totalShards; + return result; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStats.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStats.java index 76f6b219184..6cb56bd88dc 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStats.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStats.java @@ -26,12 +26,14 @@ import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; import java.io.IOException; -public class SnapshotStats implements Streamable, ToXContentFragment { +public class SnapshotStats implements Streamable, ToXContentObject { private long startTime; private long time; @@ -176,35 +178,132 @@ public class SnapshotStats implements Streamable, ToXContentFragment { @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(Fields.STATS) - // incremental starts - .startObject(Fields.INCREMENTAL) - .field(Fields.FILE_COUNT, getIncrementalFileCount()) - .humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getIncrementalSize())) - // incremental ends - .endObject(); + builder.startObject(); + { + builder.startObject(Fields.INCREMENTAL); + { + builder.field(Fields.FILE_COUNT, getIncrementalFileCount()); + builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getIncrementalSize())); + } + builder.endObject(); - if (getProcessedFileCount() != getIncrementalFileCount()) { - // processed starts - builder.startObject(Fields.PROCESSED) - .field(Fields.FILE_COUNT, getProcessedFileCount()) - .humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getProcessedSize())) - // processed ends - .endObject(); + if (getProcessedFileCount() != getIncrementalFileCount()) { + builder.startObject(Fields.PROCESSED); + { + builder.field(Fields.FILE_COUNT, getProcessedFileCount()); + builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getProcessedSize())); + } + builder.endObject(); + } + + builder.startObject(Fields.TOTAL); + { + builder.field(Fields.FILE_COUNT, getTotalFileCount()); + builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getTotalSize())); + } + builder.endObject(); + + // timings stats + builder.field(Fields.START_TIME_IN_MILLIS, getStartTime()); + builder.humanReadableField(Fields.TIME_IN_MILLIS, Fields.TIME, new TimeValue(getTime())); } - // total starts - builder.startObject(Fields.TOTAL) - .field(Fields.FILE_COUNT, getTotalFileCount()) - .humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, new ByteSizeValue(getTotalSize())) - // total ends - .endObject(); - // timings stats - builder.field(Fields.START_TIME_IN_MILLIS, getStartTime()) - .humanReadableField(Fields.TIME_IN_MILLIS, Fields.TIME, new TimeValue(getTime())); - return builder.endObject(); } + public static SnapshotStats fromXContent(XContentParser parser) throws IOException { + // Parse this old school style instead of using the ObjectParser since there's an impedance mismatch between how the + // object has historically been written as JSON versus how it is structured in Java. + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + long startTime = 0; + long time = 0; + int incrementalFileCount = 0; + int totalFileCount = 0; + int processedFileCount = 0; + long incrementalSize = 0; + long totalSize = 0; + long processedSize = 0; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + String currentName = parser.currentName(); + token = parser.nextToken(); + if (currentName.equals(Fields.INCREMENTAL)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + String innerName = parser.currentName(); + token = parser.nextToken(); + if (innerName.equals(Fields.FILE_COUNT)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + incrementalFileCount = parser.intValue(); + } else if (innerName.equals(Fields.SIZE_IN_BYTES)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + incrementalSize = parser.longValue(); + } else { + // Unknown sub field, skip + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + parser.skipChildren(); + } + } + } + } else if (currentName.equals(Fields.PROCESSED)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + String innerName = parser.currentName(); + token = parser.nextToken(); + if (innerName.equals(Fields.FILE_COUNT)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + processedFileCount = parser.intValue(); + } else if (innerName.equals(Fields.SIZE_IN_BYTES)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + processedSize = parser.longValue(); + } else { + // Unknown sub field, skip + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + parser.skipChildren(); + } + } + } + } else if (currentName.equals(Fields.TOTAL)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + String innerName = parser.currentName(); + token = parser.nextToken(); + if (innerName.equals(Fields.FILE_COUNT)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + totalFileCount = parser.intValue(); + } else if (innerName.equals(Fields.SIZE_IN_BYTES)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + totalSize = parser.longValue(); + } else { + // Unknown sub field, skip + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + parser.skipChildren(); + } + } + } + } else if (currentName.equals(Fields.START_TIME_IN_MILLIS)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + startTime = parser.longValue(); + } else if (currentName.equals(Fields.TIME_IN_MILLIS)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + time = parser.longValue(); + } else { + // Unknown field, skip + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + parser.skipChildren(); + } + } + } + return new SnapshotStats(startTime, time, incrementalFileCount, totalFileCount, processedFileCount, incrementalSize, totalSize, + processedSize); + } + void add(SnapshotStats stats) { incrementalFileCount += stats.incrementalFileCount; totalFileCount += stats.totalFileCount; @@ -229,4 +328,34 @@ public class SnapshotStats implements Streamable, ToXContentFragment { time = endTime - startTime; } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotStats that = (SnapshotStats) o; + + if (startTime != that.startTime) return false; + if (time != that.time) return false; + if (incrementalFileCount != that.incrementalFileCount) return false; + if (totalFileCount != that.totalFileCount) return false; + if (processedFileCount != that.processedFileCount) return false; + if (incrementalSize != that.incrementalSize) return false; + if (totalSize != that.totalSize) return false; + return processedSize == that.processedSize; + } + + @Override + public int hashCode() { + int result = (int) (startTime ^ (startTime >>> 32)); + result = 31 * result + (int) (time ^ (time >>> 32)); + result = 31 * result + incrementalFileCount; + result = 31 * result + totalFileCount; + result = 31 * result + processedFileCount; + result = 31 * result + (int) (incrementalSize ^ (incrementalSize >>> 32)); + result = 31 * result + (int) (totalSize ^ (totalSize >>> 32)); + result = 31 * result + (int) (processedSize ^ (processedSize >>> 32)); + return result; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java index f7545ea0236..618bb54c901 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java @@ -20,15 +20,21 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; import org.elasticsearch.Version; +import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.SnapshotsInProgress.State; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.snapshots.Snapshot; +import org.elasticsearch.snapshots.SnapshotId; import java.io.IOException; import java.util.ArrayList; @@ -40,7 +46,11 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * Status of a snapshot @@ -72,6 +82,18 @@ public class SnapshotStatus implements ToXContentObject, Streamable { updateShardStats(); } + private SnapshotStatus(Snapshot snapshot, State state, List shards, + Map indicesStatus, SnapshotShardsStats shardsStats, + SnapshotStats stats, Boolean includeGlobalState) { + this.snapshot = snapshot; + this.state = state; + this.shards = shards; + this.indicesStatus = indicesStatus; + this.shardsStats = shardsStats; + this.stats = stats; + this.includeGlobalState = includeGlobalState; + } + SnapshotStatus() { } @@ -207,8 +229,8 @@ public class SnapshotStatus implements ToXContentObject, Streamable { if (includeGlobalState != null) { builder.field(INCLUDE_GLOBAL_STATE, includeGlobalState); } - shardsStats.toXContent(builder, params); - stats.toXContent(builder, params); + builder.field(SnapshotShardsStats.Fields.SHARDS_STATS, shardsStats, params); + builder.field(SnapshotStats.Fields.STATS, stats, params); builder.startObject(INDICES); for (SnapshotIndexStatus indexStatus : getIndices().values()) { indexStatus.toXContent(builder, params); @@ -218,6 +240,52 @@ public class SnapshotStatus implements ToXContentObject, Streamable { return builder; } + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "snapshot_status", true, + (Object[] parsedObjects) -> { + int i = 0; + String name = (String) parsedObjects[i++]; + String repository = (String) parsedObjects[i++]; + String uuid = (String) parsedObjects[i++]; + String rawState = (String) parsedObjects[i++]; + Boolean includeGlobalState = (Boolean) parsedObjects[i++]; + SnapshotStats stats = ((SnapshotStats) parsedObjects[i++]); + SnapshotShardsStats shardsStats = ((SnapshotShardsStats) parsedObjects[i++]); + @SuppressWarnings("unchecked") List indices = ((List) parsedObjects[i]); + + Snapshot snapshot = new Snapshot(repository, new SnapshotId(name, uuid)); + SnapshotsInProgress.State state = SnapshotsInProgress.State.valueOf(rawState); + Map indicesStatus; + List shards; + if (indices == null || indices.isEmpty()) { + indicesStatus = emptyMap(); + shards = emptyList(); + } else { + indicesStatus = new HashMap<>(indices.size()); + shards = new ArrayList<>(); + for (SnapshotIndexStatus index : indices) { + indicesStatus.put(index.getIndex(), index); + shards.addAll(index.getShards().values()); + } + } + return new SnapshotStatus(snapshot, state, shards, indicesStatus, shardsStats, stats, includeGlobalState); + }); + static { + PARSER.declareString(constructorArg(), new ParseField(SNAPSHOT)); + PARSER.declareString(constructorArg(), new ParseField(REPOSITORY)); + PARSER.declareString(constructorArg(), new ParseField(UUID)); + PARSER.declareString(constructorArg(), new ParseField(STATE)); + PARSER.declareBoolean(optionalConstructorArg(), new ParseField(INCLUDE_GLOBAL_STATE)); + PARSER.declareField(constructorArg(), SnapshotStats::fromXContent, new ParseField(SnapshotStats.Fields.STATS), + ObjectParser.ValueType.OBJECT); + PARSER.declareObject(constructorArg(), SnapshotShardsStats.PARSER, new ParseField(SnapshotShardsStats.Fields.SHARDS_STATS)); + PARSER.declareNamedObjects(constructorArg(), SnapshotIndexStatus.PARSER, new ParseField(INDICES)); + } + + public static SnapshotStatus fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + private void updateShardStats() { stats = new SnapshotStats(); shardsStats = new SnapshotShardsStats(shards); @@ -225,4 +293,31 @@ public class SnapshotStatus implements ToXContentObject, Streamable { stats.add(shard.getStats()); } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotStatus that = (SnapshotStatus) o; + + if (snapshot != null ? !snapshot.equals(that.snapshot) : that.snapshot != null) return false; + if (state != that.state) return false; + if (indicesStatus != null ? !indicesStatus.equals(that.indicesStatus) : that.indicesStatus != null) + return false; + if (shardsStats != null ? !shardsStats.equals(that.shardsStats) : that.shardsStats != null) return false; + if (stats != null ? !stats.equals(that.stats) : that.stats != null) return false; + return includeGlobalState != null ? includeGlobalState.equals(that.includeGlobalState) : that.includeGlobalState == null; + } + + @Override + public int hashCode() { + int result = snapshot != null ? snapshot.hashCode() : 0; + result = 31 * result + (state != null ? state.hashCode() : 0); + result = 31 * result + (indicesStatus != null ? indicesStatus.hashCode() : 0); + result = 31 * result + (shardsStats != null ? shardsStats.hashCode() : 0); + result = 31 * result + (stats != null ? stats.hashCode() : 0); + result = 31 * result + (includeGlobalState != null ? includeGlobalState.hashCode() : 0); + return result; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java index d44a490680c..ef1435e4108 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java @@ -20,16 +20,21 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +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 java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + /** * Snapshot status response */ @@ -85,4 +90,33 @@ public class SnapshotsStatusResponse extends ActionResponse implements ToXConten return builder; } + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "snapshots_status_response", true, + (Object[] parsedObjects) -> { + @SuppressWarnings("unchecked") List snapshots = (List) parsedObjects[0]; + return new SnapshotsStatusResponse(snapshots); + } + ); + static { + PARSER.declareObjectArray(constructorArg(), SnapshotStatus.PARSER, new ParseField("snapshots")); + } + + public static SnapshotsStatusResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SnapshotsStatusResponse response = (SnapshotsStatusResponse) o; + + return snapshots != null ? snapshots.equals(response.snapshots) : response.snapshots == null; + } + + @Override + public int hashCode() { + return snapshots != null ? snapshots.hashCode() : 0; + } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatusTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatusTests.java new file mode 100644 index 00000000000..490319ef840 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexShardStatusTests.java @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.snapshots.status; + +import java.io.IOException; +import java.util.function.Predicate; + +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.AbstractXContentTestCase; + +public class SnapshotIndexShardStatusTests extends AbstractXContentTestCase { + + @Override + protected SnapshotIndexShardStatus createTestInstance() { + return createForIndex(randomAlphaOfLength(10)); + } + + protected SnapshotIndexShardStatus createForIndex(String indexName) { + ShardId shardId = new ShardId(new Index(indexName, IndexMetaData.INDEX_UUID_NA_VALUE), randomIntBetween(0, 500)); + SnapshotIndexShardStage stage = randomFrom(SnapshotIndexShardStage.values()); + SnapshotStats stats = new SnapshotStatsTests().createTestInstance(); + String nodeId = randomAlphaOfLength(20); + String failure = null; + if (rarely()) { + failure = randomAlphaOfLength(200); + } + return new SnapshotIndexShardStatus(shardId, stage, stats, nodeId, failure); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + // Do not place random fields in the root object since its fields correspond to shard names. + return String::isEmpty; + } + + @Override + protected SnapshotIndexShardStatus doParseInstance(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation); + SnapshotIndexShardStatus status = SnapshotIndexShardStatus.fromXContent(parser, parser.currentName()); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); + return status; + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatusTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatusTests.java new file mode 100644 index 00000000000..92eb355f3a6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotIndexStatusTests.java @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.snapshots.status; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.test.AbstractXContentTestCase; + + +public class SnapshotIndexStatusTests extends AbstractXContentTestCase { + + @Override + protected SnapshotIndexStatus createTestInstance() { + String index = randomAlphaOfLength(10); + List shardStatuses = new ArrayList<>(); + SnapshotIndexShardStatusTests builder = new SnapshotIndexShardStatusTests(); + for (int idx = 0; idx < randomIntBetween(0, 10); idx++) { + shardStatuses.add(builder.createForIndex(index)); + } + return new SnapshotIndexStatus(index, shardStatuses); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + // Do not place random fields in the root object or the shards field since their fields correspond to names. + return (s) -> s.isEmpty() || s.endsWith("shards"); + } + + @Override + protected SnapshotIndexStatus doParseInstance(XContentParser parser) throws IOException { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation); + SnapshotIndexStatus status = SnapshotIndexStatus.fromXContent(parser); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); + return status; + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStatsTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStatsTests.java new file mode 100644 index 00000000000..ac00896983d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotShardsStatsTests.java @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.snapshots.status; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +public class SnapshotShardsStatsTests extends AbstractXContentTestCase { + + @Override + protected SnapshotShardsStats createTestInstance() { + int initializingShards = randomInt(); + int startedShards = randomInt(); + int finalizingShards = randomInt(); + int doneShards = randomInt(); + int failedShards = randomInt(); + int totalShards = randomInt(); + return new SnapshotShardsStats(initializingShards, startedShards, finalizingShards, doneShards, failedShards, totalShards); + } + + @Override + protected SnapshotShardsStats doParseInstance(XContentParser parser) throws IOException { + return SnapshotShardsStats.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatsTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatsTests.java new file mode 100644 index 00000000000..2822a9661fd --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatsTests.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.snapshots.status; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +public class SnapshotStatsTests extends AbstractXContentTestCase { + + @Override + protected SnapshotStats createTestInstance() { + long startTime = randomNonNegativeLong(); + long time = randomNonNegativeLong(); + int incrementalFileCount = randomIntBetween(0, Integer.MAX_VALUE); + int totalFileCount = randomIntBetween(0, Integer.MAX_VALUE); + int processedFileCount = randomIntBetween(0, Integer.MAX_VALUE); + long incrementalSize = ((long)randomIntBetween(0, Integer.MAX_VALUE)) * 2; + long totalSize = ((long)randomIntBetween(0, Integer.MAX_VALUE)) * 2; + long processedSize = ((long)randomIntBetween(0, Integer.MAX_VALUE)) * 2; + return new SnapshotStats(startTime, time, incrementalFileCount, totalFileCount, + processedFileCount, incrementalSize, totalSize, processedSize); + } + + @Override + protected SnapshotStats doParseInstance(XContentParser parser) throws IOException { + return SnapshotStats.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatusTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatusTests.java index 3ece0f9f107..dbd45640c7b 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatusTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatusTests.java @@ -21,16 +21,19 @@ package org.elasticsearch.action.admin.cluster.snapshots.status; import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.AbstractXContentTestCase; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; -public class SnapshotStatusTests extends ESTestCase { +public class SnapshotStatusTests extends AbstractXContentTestCase { public void testToString() throws Exception { @@ -146,4 +149,39 @@ public class SnapshotStatusTests extends ESTestCase { "}"; assertEquals(expected, status.toString()); } + + @Override + protected SnapshotStatus createTestInstance() { + SnapshotsInProgress.State state = randomFrom(SnapshotsInProgress.State.values()); + String uuid = UUIDs.randomBase64UUID(); + SnapshotId id = new SnapshotId("test-snap", uuid); + Snapshot snapshot = new Snapshot("test-repo", id); + + SnapshotIndexShardStatusTests builder = new SnapshotIndexShardStatusTests(); + builder.createTestInstance(); + + List snapshotIndexShardStatuses = new ArrayList<>(); + for (int idx = 0; idx < randomIntBetween(0, 10); idx++) { + SnapshotIndexShardStatus snapshotIndexShardStatus = builder.createTestInstance(); + snapshotIndexShardStatuses.add(snapshotIndexShardStatus); + } + boolean includeGlobalState = randomBoolean(); + return new SnapshotStatus(snapshot, state, snapshotIndexShardStatuses, includeGlobalState); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + // Do not place random fields in the indices field or shards field since their fields correspond to names. + return (s) -> s.endsWith("shards") || s.endsWith("indices"); + } + + @Override + protected SnapshotStatus doParseInstance(XContentParser parser) throws IOException { + return SnapshotStatus.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java new file mode 100644 index 00000000000..d1ad028296d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.cluster.snapshots.status; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +public class SnapshotsStatusResponseTests extends AbstractXContentTestCase { + + @Override + protected SnapshotsStatusResponse doParseInstance(XContentParser parser) throws IOException { + return SnapshotsStatusResponse.fromXContent(parser); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + // Do not place random fields in the indices field or shards field since their fields correspond to names. + return (s) -> s.endsWith("shards") || s.endsWith("indices"); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + @Override + protected SnapshotsStatusResponse createTestInstance() { + SnapshotStatusTests statusBuilder = new SnapshotStatusTests(); + List snapshotStatuses = new ArrayList<>(); + for (int idx = 0; idx < randomIntBetween(0, 5); idx++) { + snapshotStatuses.add(statusBuilder.createTestInstance()); + } + return new SnapshotsStatusResponse(snapshotStatuses); + } +}