diff --git a/docs/changelog/97700.yaml b/docs/changelog/97700.yaml new file mode 100644 index 0000000000000..0e144a22ab39b --- /dev/null +++ b/docs/changelog/97700.yaml @@ -0,0 +1,6 @@ +pr: 97700 +summary: Add `completion_time` time field to `async_search` get and status response +area: Search +type: enhancement +issues: + - 88640 diff --git a/docs/reference/search/async-search.asciidoc b/docs/reference/search/async-search.asciidoc index 464d0cbc1ccc6..9d34077d38021 100644 --- a/docs/reference/search/async-search.asciidoc +++ b/docs/reference/search/async-search.asciidoc @@ -70,6 +70,7 @@ search results are returned as part of the // TESTRESPONSE[s/"is_running" : true/"is_running": $body.is_running/] // TESTRESPONSE[s/1583945890986/$body.start_time_in_millis/] // TESTRESPONSE[s/1584377890986/$body.expiration_time_in_millis/] +// TESTRESPONSE[s/"response"/"completion_time_in_millis": $body.completion_time_in_millis,\n "response"/] // TESTRESPONSE[s/"took" : 1122/"took": $body.response.took/] // TESTRESPONSE[s/"num_reduce_phases" : 0,//] // TESTRESPONSE[s/"total" : 562/"total": $body.response._shards.total/] @@ -156,17 +157,18 @@ GET /_async_search/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsd -------------------------------------------------- { "id" : "FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsdzoxMDc=", - "is_partial" : true, <1> - "is_running" : true, <2> + "is_partial" : false, <1> + "is_running" : false, <2> "start_time_in_millis" : 1583945890986, "expiration_time_in_millis" : 1584377890986, <3> + "completion_time_in_millis" : 1583945903130, <4> "response" : { "took" : 12144, "timed_out" : false, - "num_reduce_phases" : 46, <4> + "num_reduce_phases" : 46, <5> "_shards" : { "total" : 562, - "successful" : 188, <5> + "successful" : 188, <6> "skipped" : 0, "failed" : 0 }, @@ -178,7 +180,7 @@ GET /_async_search/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsd "max_score" : null, "hits" : [ ] }, - "aggregations" : { <6> + "aggregations" : { <7> "sale_date" : { "buckets" : [] } @@ -191,6 +193,7 @@ GET /_async_search/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsd // TESTRESPONSE[s/"is_running" : true/"is_running" : false/] // TESTRESPONSE[s/1583945890986/$body.start_time_in_millis/] // TESTRESPONSE[s/1584377890986/$body.expiration_time_in_millis/] +// TESTRESPONSE[s/1583945903130/$body.completion_time_in_millis/] // TESTRESPONSE[s/"took" : 12144/"took": $body.response.took/] // TESTRESPONSE[s/"total" : 562/"total": $body.response._shards.total/] // TESTRESPONSE[s/"successful" : 188/"successful": $body.response._shards.successful/] @@ -203,13 +206,14 @@ or was successfully completed on all shards. While the query is being executed, `is_partial` is always set to `true` <2> Whether the search is still being executed or it has completed <3> When the async search will expire -<4> Indicates how many reductions of the results have been performed. If this +<4> When the async search has finished, the completion_time is indicated (start_time + took) +<5> Indicates how many reductions of the results have been performed. If this number increases compared to the last retrieved results, you can expect additional results included in the search response -<5> Indicates how many shards have executed the query. Note that in order for +<6> Indicates how many shards have executed the query. Note that in order for shard results to be included in the search response, they need to be reduced first. -<6> Partial aggregations results, coming from the shards that have already +<7> Partial aggregations results, coming from the shards that have already completed the execution of the query. The `wait_for_completion_timeout` parameter can also be provided when calling diff --git a/docs/reference/search/search-your-data/search-across-clusters.asciidoc b/docs/reference/search/search-your-data/search-across-clusters.asciidoc index 9ed29138a3771..6c478ec3ca9e0 100644 --- a/docs/reference/search/search-your-data/search-across-clusters.asciidoc +++ b/docs/reference/search/search-your-data/search-across-clusters.asciidoc @@ -365,6 +365,7 @@ The API returns the following response: // TESTRESPONSE[s/"is_running": true/"is_running": $body.is_running/] // TESTRESPONSE[s/1685563581380/$body.start_time_in_millis/] // TESTRESPONSE[s/1685995581380/$body.expiration_time_in_millis/] +// TESTRESPONSE[s/"response"/"completion_time_in_millis": $body.completion_time_in_millis,\n "response"/] // TESTRESPONSE[s/"num_reduce_phases": 0/"num_reduce_phases": "$body.response.num_reduce_phases"/] // TESTRESPONSE[s/"took": 1020/"took": "$body.response.took"/] // TESTRESPONSE[s/"total": 8/"total": $body.response._shards.total/] @@ -465,19 +466,20 @@ Response: "is_running": false, "start_time_in_millis": 1685564911108, "expiration_time_in_millis": 1685996911108, + "completion_time_in_millis": 1685564938727, <1> "response": { "took": 27619, "timed_out": false, "num_reduce_phases": 4, "_shards": { "total": 28, - "successful": 28, <1> + "successful": 28, <2> "skipped": 0, "failed": 0 }, "_clusters": { "total": 3, - "successful": 3, <2> + "successful": 3, <3> "skipped": 0 }, "hits": { @@ -496,6 +498,7 @@ Response: // TESTRESPONSE[s/"is_running": true/"is_running": $body.is_running/] // TESTRESPONSE[s/1685564911108/$body.start_time_in_millis/] // TESTRESPONSE[s/1685996911108/$body.expiration_time_in_millis/] +// TESTRESPONSE[s/1685564938727/$body.completion_time_in_millis/] // TESTRESPONSE[s/"took": 27619/"took": "$body.response.took"/] // TESTRESPONSE[s/"total": 28/"total": $body.response._shards.total/] // TESTRESPONSE[s/"successful": 28/"successful": $body.response._shards.successful/] @@ -506,9 +509,10 @@ Response: // TESTRESPONSE[s/"hits": \[...list of hits here...\]/"hits": $body.response.hits.hits/] -<1> The `_shards` section is now updated to show that 28 total shards +<1> Once the search has finished, the completion_time is present. +<2> The `_shards` section is now updated to show that 28 total shards were searched across all clusters and that all were successful. -<2> The `_clusters` section shows that searches on all 3 clusters were successful. +<3> The `_clusters` section shows that searches on all 3 clusters were successful. @@ -592,6 +596,7 @@ the `wait_for_completion_timeout` duration (see <>). // TESTRESPONSE[s/"is_running": true/"is_running": $body.is_running/] // TESTRESPONSE[s/1685563581380/$body.start_time_in_millis/] // TESTRESPONSE[s/1685995581380/$body.expiration_time_in_millis/] +// TESTRESPONSE[s/"response"/"completion_time_in_millis": $body.completion_time_in_millis,\n "response"/] // TESTRESPONSE[s/"num_reduce_phases": 0/"num_reduce_phases": "$body.response.num_reduce_phases"/] // TESTRESPONSE[s/"took": 1020/"took": "$body.response.took"/] // TESTRESPONSE[s/"total": 28/"total": $body.response._shards.total/] diff --git a/server/src/main/java/org/elasticsearch/TransportVersion.java b/server/src/main/java/org/elasticsearch/TransportVersion.java index 174130a8a4b2b..16919395ca918 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersion.java +++ b/server/src/main/java/org/elasticsearch/TransportVersion.java @@ -155,9 +155,10 @@ private static TransportVersion registerTransportVersion(int id, String uniqueId public static final TransportVersion V_8_500_032 = registerTransportVersion(8_500_032, "a9a14bc6-c3f2-41d9-a3d8-c686bf2c901d"); public static final TransportVersion V_8_500_033 = registerTransportVersion(8_500_033, "193ab7c4-a751-4cbd-a66a-2d7d56ccbc10"); public static final TransportVersion V_8_500_034 = registerTransportVersion(8_500_034, "16871c8b-88ba-4432-980a-10fd9ecad2dc"); + public static final TransportVersion V_8_500_035 = registerTransportVersion(8_500_035, "664dd6ce-3487-4fbd-81a9-af778b28be45"); private static class CurrentHolder { - private static final TransportVersion CURRENT = findCurrent(V_8_500_034); + private static final TransportVersion CURRENT = findCurrent(V_8_500_035); // finds the pluggable current version, or uses the given fallback private static TransportVersion findCurrent(TransportVersion fallback) { diff --git a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java index 5a3c07ca521fb..6ec62a5923c49 100644 --- a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java +++ b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java @@ -212,7 +212,7 @@ synchronized AsyncSearchResponse toAsyncSearchResponse(AsyncSearchTask task, lon } else { /* * Build the response, reducing aggs if we haven't already and - * storing the result of the reduction so we won't have to reduce + * storing the result of the reduction, so we won't have to reduce * the same aggregation results a second time if nothing has changed. * This does cost memory because we have a reference to the finally * reduced aggs sitting around which can't be GCed until we get an update. @@ -253,6 +253,7 @@ synchronized AsyncStatusResponse toStatusResponse(String asyncExecutionId, long false, startTime, expirationTime, + startTime + finalResponse.getTook().millis(), finalResponse.getTotalShards(), finalResponse.getSuccessfulShards(), finalResponse.getSkippedShards(), @@ -268,6 +269,7 @@ synchronized AsyncStatusResponse toStatusResponse(String asyncExecutionId, long true, startTime, expirationTime, + null, totalShards, successfulShards, skippedShards, @@ -282,6 +284,7 @@ synchronized AsyncStatusResponse toStatusResponse(String asyncExecutionId, long true, startTime, expirationTime, + null, totalShards, successfulShards, skippedShards, diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java index 557006b83b16e..3ed5a51ee0417 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.SearchResponseSections; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -16,6 +17,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentElasticsearchExtension; import org.elasticsearch.script.ScriptException; +import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.internal.InternalSearchResponse; import org.elasticsearch.test.ESTestCase; @@ -26,6 +28,7 @@ import org.junit.Before; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -138,7 +141,7 @@ static void assertEqualResponses(AsyncSearchResponse expected, AsyncSearchRespon assertEquals(expected.getExpirationTime(), actual.getExpirationTime()); } - public void testToXContent() throws IOException { + public void testToXContentWithoutSearchResponse() throws IOException { Date date = new Date(); AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse("id", true, true, date.getTime(), date.getTime()); @@ -180,4 +183,210 @@ public void testToXContent() throws IOException { ); } } + + // completion_time should be present since search has completed + public void testToXContentWithSearchResponseAfterCompletion() throws IOException { + boolean isRunning = false; + long startTimeMillis = 1689352924517L; + long expirationTimeMillis = 1689784924517L; + long took = 22968L; + long expectedCompletionTime = startTimeMillis + took; + + SearchHits hits = SearchHits.EMPTY_WITHOUT_TOTAL_HITS; + SearchResponseSections sections = new SearchResponseSections(hits, null, null, false, null, null, 2); + SearchResponse searchResponse = new SearchResponse( + sections, + null, + 10, + 9, + 1, + took, + new ShardSearchFailure[0], + SearchResponse.Clusters.EMPTY + ); + + AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + false, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + asyncSearchResponse.toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", startTimeMillis, expirationTimeMillis, expectedCompletionTime, took), Strings.toString(builder)); + } + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + asyncSearchResponse.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "completion_time" : "%s", + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expectedCompletionTime)), + expectedCompletionTime, + took + ), + Strings.toString(builder) + ); + } + } + + // completion_time should NOT be present since search is still running + public void testToXContentWithSearchResponseWhileRunning() throws IOException { + boolean isRunning = true; + long startTimeMillis = 1689352924517L; + long expirationTimeMillis = 1689784924517L; + long took = 22968L; + + SearchHits hits = SearchHits.EMPTY_WITHOUT_TOTAL_HITS; + SearchResponseSections sections = new SearchResponseSections(hits, null, null, false, null, null, 2); + SearchResponse searchResponse = new SearchResponse( + sections, + null, + 10, + 9, + 1, + took, + new ShardSearchFailure[0], + SearchResponse.Clusters.EMPTY + ); + + AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + true, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + asyncSearchResponse.toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", startTimeMillis, expirationTimeMillis, took), Strings.toString(builder)); + } + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + asyncSearchResponse.toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + took + ), + Strings.toString(builder) + ); + } + } } diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java index b7561a166154b..3e4aa163660b1 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java @@ -40,6 +40,10 @@ protected AsyncStatusResponse createTestInstance() { int skippedShards = randomIntBetween(0, 5); int failedShards = totalShards - successfulShards - skippedShards; RestStatus completionStatus = isRunning ? null : randomBoolean() ? RestStatus.OK : RestStatus.SERVICE_UNAVAILABLE; + Long completionTimeMillis = null; + if (isRunning == false && completionStatus == RestStatus.OK) { + completionTimeMillis = startTimeMillis + 25000; + } SearchResponse.Clusters clusters = switch (randomIntBetween(0, 3)) { case 1 -> SearchResponse.Clusters.EMPTY; case 2 -> new SearchResponse.Clusters(1, 1, 0); @@ -52,6 +56,7 @@ protected AsyncStatusResponse createTestInstance() { isPartial, startTimeMillis, expirationTimeMillis, + completionTimeMillis, totalShards, successfulShards, skippedShards, @@ -84,6 +89,7 @@ protected AsyncStatusResponse mutateInstance(AsyncStatusResponse instance) { isPartial, instance.getStartTime(), instance.getExpirationTime(), + isRunning ? null : instance.getStartTime() + 25000, instance.getTotalShards(), instance.getSuccessfulShards(), instance.getSkippedShards(), @@ -95,6 +101,10 @@ protected AsyncStatusResponse mutateInstance(AsyncStatusResponse instance) { public void testToXContent() throws IOException { AsyncStatusResponse response = createTestInstance(); + String completionTimeEntry = ""; + if (response.isRunning() == false && response.getCompletionStatus() == RestStatus.OK) { + completionTimeEntry = Strings.format("\"completion_time_in_millis\" : %s,", response.getCompletionTime()); + } try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { String expectedJson; SearchResponse.Clusters clusters = response.getClusters(); @@ -105,6 +115,7 @@ public void testToXContent() throws IOException { response.isPartial(), response.getStartTime(), response.getExpirationTime(), + completionTimeEntry, response.getTotalShards(), response.getSuccessfulShards(), response.getSkippedShards(), @@ -119,6 +130,7 @@ public void testToXContent() throws IOException { "is_partial" : %s, "start_time_in_millis" : %s, "expiration_time_in_millis" : %s, + %s "_shards" : { "total" : %s, "successful" : %s, @@ -135,6 +147,7 @@ public void testToXContent() throws IOException { response.isPartial(), response.getStartTime(), response.getExpirationTime(), + completionTimeEntry, response.getTotalShards(), response.getSuccessfulShards(), response.getSkippedShards(), @@ -152,6 +165,7 @@ public void testToXContent() throws IOException { "is_partial" : %s, "start_time_in_millis" : %s, "expiration_time_in_millis" : %s, + %s "_shards" : { "total" : %s, "successful" : %s, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java index 93db13ee4180d..cddd6097b9477 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncSearchResponse.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.StatusToXContentObject; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.async.AsyncResponse; @@ -165,6 +166,18 @@ public long getExpirationTime() { return expirationTimeMillis; } + /** + * @return completion time in millis if the search is finished running. + * Otherwise it will return null; + */ + public Long getCompletionTime() { + if (searchResponse == null || isRunning) { + return null; + } else { + return getStartTime() + searchResponse.getTook().millis(); + } + } + @Override public AsyncSearchResponse withExpirationTime(long expirationTime) { return new AsyncSearchResponse(id, searchResponse, error, isPartial, isRunning, startTimeMillis, expirationTime); @@ -194,6 +207,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.timeField("expiration_time_in_millis", "expiration_time", expirationTimeMillis); if (searchResponse != null) { + if (isRunning == false) { + TimeValue took = searchResponse.getTook(); + builder.timeField("completion_time_in_millis", "completion_time", startTimeMillis + took.millis()); + } builder.field("response"); ChunkedToXContent.wrapAsToXContent(searchResponse).toXContent(builder, params); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncStatusResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncStatusResponse.java index 0be988a27c616..43118eff57fd8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncStatusResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/AsyncStatusResponse.java @@ -32,6 +32,7 @@ public class AsyncStatusResponse extends ActionResponse implements SearchStatusR private final boolean isPartial; private final long startTimeMillis; private final long expirationTimeMillis; + private final Long completionTimeMillis; private final int totalShards; private final int successfulShards; private final int skippedShards; @@ -48,6 +49,7 @@ public AsyncStatusResponse( boolean isPartial, long startTimeMillis, long expirationTimeMillis, + Long completionTimeMillis, int totalShards, int successfulShards, int skippedShards, @@ -60,6 +62,7 @@ public AsyncStatusResponse( this.isPartial = isPartial; this.startTimeMillis = startTimeMillis; this.expirationTimeMillis = expirationTimeMillis; + this.completionTimeMillis = completionTimeMillis; this.totalShards = totalShards; this.successfulShards = successfulShards; this.skippedShards = skippedShards; @@ -112,6 +115,7 @@ public static AsyncStatusResponse getStatusFromStoredSearch( asyncSearchResponse.isPartial(), asyncSearchResponse.getStartTime(), expirationTimeMillis, + asyncSearchResponse.getCompletionTime(), totalShards, successfulShards, skippedShards, @@ -137,6 +141,11 @@ public AsyncStatusResponse(StreamInput in) throws IOException { } else { this.clusters = null; } + if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_500_035)) { + this.completionTimeMillis = in.readOptionalVLong(); + } else { + this.completionTimeMillis = null; + } } @Override @@ -157,6 +166,9 @@ public void writeTo(StreamOutput out) throws IOException { // optional since only CCS uses is; it is null for local-only searches out.writeOptionalWriteable(clusters); } + if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_500_035)) { + out.writeOptionalVLong(completionTimeMillis); + } } @Override @@ -172,6 +184,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("is_partial", isPartial); builder.timeField("start_time_in_millis", "start_time", startTimeMillis); builder.timeField("expiration_time_in_millis", "expiration_time", expirationTimeMillis); + if (completionTimeMillis != null) { + builder.timeField("completion_time_in_millis", "completion_time", completionTimeMillis); + } RestActions.buildBroadcastShardsHeader(builder, params, totalShards, successfulShards, skippedShards, failedShards, null); if (clusters != null) { builder = clusters.toXContent(builder, null); @@ -257,6 +272,13 @@ public long getExpirationTime() { return expirationTimeMillis; } + /** + * @return completion_time_in_millis if set, otherwise null + */ + public Long getCompletionTime() { + return completionTimeMillis; + } + /** * Returns the total number of shards the search is executed on. */