Explorar el Código

Rest High Level client: Add List Tasks (#29546)

This change adds a `listTasks` method to the high level java
ClusterClient which allows listing running tasks through the 
task management API.

Related to #27205
Van0SS hace 7 años
padre
commit
4478f10a2a
Se han modificado 19 ficheros con 808 adiciones y 140 borrados
  1. 24 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java
  2. 54 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
  3. 31 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java
  4. 126 69
      client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
  5. 95 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java
  6. 101 0
      docs/java-rest/high-level/cluster/list_tasks.asciidoc
  7. 3 1
      docs/java-rest/high-level/supported-apis.asciidoc
  8. 31 7
      server/src/main/java/org/elasticsearch/action/TaskOperationFailure.java
  9. 42 9
      server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java
  10. 4 4
      server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksResponse.java
  11. 3 4
      server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java
  12. 5 0
      server/src/main/java/org/elasticsearch/tasks/TaskInfo.java
  13. 63 0
      server/src/test/java/org/elasticsearch/action/TaskOperationFailureTests.java
  14. 2 1
      server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java
  15. 60 5
      server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java
  16. 156 0
      server/src/test/java/org/elasticsearch/tasks/TaskInfoTests.java
  17. 2 33
      server/src/test/java/org/elasticsearch/tasks/TaskResultTests.java
  18. 4 4
      test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java
  19. 2 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java

+ 24 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ClusterClient.java

@@ -21,6 +21,8 @@ package org.elasticsearch.client;
 
 import org.apache.http.Header;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse;
 
@@ -63,4 +65,26 @@ public final class ClusterClient {
         restHighLevelClient.performRequestAsyncAndParseEntity(clusterUpdateSettingsRequest, RequestConverters::clusterPutSettings,
                 ClusterUpdateSettingsResponse::fromXContent, listener, emptySet(), headers);
     }
+
+    /**
+     * Get current tasks using the Task Management API
+     * <p>
+     * See
+     * <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html"> Task Management API on elastic.co</a>
+     */
+    public ListTasksResponse listTasks(ListTasksRequest request, Header... headers) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, RequestConverters::listTasks, ListTasksResponse::fromXContent,
+                emptySet(), headers);
+    }
+
+    /**
+     * Asynchronously get current tasks using the Task Management API
+     * <p>
+     * See
+     * <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html"> Task Management API on elastic.co</a>
+     */
+    public void listTasksAsync(ListTasksRequest request, ActionListener<ListTasksResponse> listener, Header... headers) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::listTasks, ListTasksResponse::fromXContent,
+                listener, emptySet(), headers);
+    }
 }

+ 54 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -29,6 +29,7 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.ContentType;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.action.DocWriteRequest;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
 import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest;
 import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
@@ -45,8 +46,8 @@ import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
 import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
-import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
 import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest;
+import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeType;
 import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest;
@@ -83,6 +84,7 @@ import org.elasticsearch.index.rankeval.RankEvalRequest;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.script.mustache.SearchTemplateRequest;
 import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
+import org.elasticsearch.tasks.TaskId;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -606,6 +608,22 @@ final class RequestConverters {
         return request;
     }
 
+    static Request listTasks(ListTasksRequest listTaskRequest) {
+        if (listTaskRequest.getTaskId() != null && listTaskRequest.getTaskId().isSet()) {
+            throw new IllegalArgumentException("TaskId cannot be used for list tasks request");
+        }
+        Request request  = new Request(HttpGet.METHOD_NAME, "/_tasks");
+        Params params = new Params(request);
+        params.withTimeout(listTaskRequest.getTimeout())
+            .withDetailed(listTaskRequest.getDetailed())
+            .withWaitForCompletion(listTaskRequest.getWaitForCompletion())
+            .withParentTaskId(listTaskRequest.getParentTaskId())
+            .withNodes(listTaskRequest.getNodes())
+            .withActions(listTaskRequest.getActions())
+            .putParam("group_by", "none");
+        return request;
+    }
+
     static Request rollover(RolloverRequest rolloverRequest) throws IOException {
         String endpoint = new EndpointBuilder().addPathPart(rolloverRequest.getAlias()).addPathPartAsIs("_rollover")
                 .addPathPart(rolloverRequest.getNewIndexName()).build();
@@ -932,6 +950,41 @@ final class RequestConverters {
             return this;
         }
 
+        Params withDetailed(boolean detailed) {
+            if (detailed) {
+                return putParam("detailed", Boolean.TRUE.toString());
+            }
+            return this;
+        }
+
+        Params withWaitForCompletion(boolean waitForCompletion) {
+            if (waitForCompletion) {
+                return putParam("wait_for_completion", Boolean.TRUE.toString());
+            }
+            return this;
+        }
+
+        Params withNodes(String[] nodes) {
+            if (nodes != null && nodes.length > 0) {
+                return putParam("nodes", String.join(",", nodes));
+            }
+            return this;
+        }
+
+        Params withActions(String[] actions) {
+            if (actions != null && actions.length > 0) {
+                return putParam("actions", String.join(",", actions));
+            }
+            return this;
+        }
+
+        Params withParentTaskId(TaskId parentTaskId) {
+            if (parentTaskId != null && parentTaskId.isSet()) {
+                return putParam("parent_task_id", parentTaskId.toString());
+            }
+            return this;
+        }
+
         Params withVerify(boolean verify) {
             if (verify) {
                 return putParam("verify", Boolean.TRUE.toString());

+ 31 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java

@@ -20,6 +20,9 @@
 package org.elasticsearch.client;
 
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.TaskGroup;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse;
 import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider;
@@ -29,13 +32,16 @@ import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.indices.recovery.RecoverySettings;
 import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.tasks.TaskInfo;
 
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
+import static java.util.Collections.emptyList;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 
@@ -105,4 +111,29 @@ public class ClusterClientIT extends ESRestHighLevelClientTestCase {
         assertThat(exception.getMessage(), equalTo(
                 "Elasticsearch exception [type=illegal_argument_exception, reason=transient setting [" + setting + "], not recognized]"));
     }
+
+    public void testListTasks() throws IOException {
+        ListTasksRequest request = new ListTasksRequest();
+        ListTasksResponse response = execute(request, highLevelClient().cluster()::listTasks, highLevelClient().cluster()::listTasksAsync);
+
+        assertThat(response, notNullValue());
+        assertThat(response.getNodeFailures(), equalTo(emptyList()));
+        assertThat(response.getTaskFailures(), equalTo(emptyList()));
+        // It's possible that there are other tasks except 'cluster:monitor/tasks/lists[n]' and 'action":"cluster:monitor/tasks/lists'
+        assertThat(response.getTasks().size(), greaterThanOrEqualTo(2));
+        boolean listTasksFound = false;
+        for (TaskGroup taskGroup : response.getTaskGroups()) {
+            TaskInfo parent = taskGroup.getTaskInfo();
+            if ("cluster:monitor/tasks/lists".equals(parent.getAction())) {
+                assertThat(taskGroup.getChildTasks().size(), equalTo(1));
+                TaskGroup childGroup = taskGroup.getChildTasks().iterator().next();
+                assertThat(childGroup.getChildTasks().isEmpty(), equalTo(true));
+                TaskInfo child = childGroup.getTaskInfo();
+                assertThat(child.getAction(), equalTo("cluster:monitor/tasks/lists[n]"));
+                assertThat(child.getParentTaskId(), equalTo(parent.getTaskId()));
+                listTasksFound = true;
+            }
+        }
+        assertTrue("List tasks were not found", listTasksFound);
+    }
 }

+ 126 - 69
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -29,6 +29,7 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.util.EntityUtils;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.DocWriteRequest;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
 import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest;
 import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
@@ -111,6 +112,7 @@ import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
 import org.elasticsearch.search.rescore.QueryRescorerBuilder;
 import org.elasticsearch.search.suggest.SuggestBuilder;
 import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
+import org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.RandomObjects;
 
@@ -142,6 +144,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.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 
 public class RequestConvertersTests extends ESTestCase {
@@ -188,8 +191,7 @@ public class RequestConvertersTests extends ESTestCase {
 
         int numberOfRequests = randomIntBetween(0, 32);
         for (int i = 0; i < numberOfRequests; i++) {
-            MultiGetRequest.Item item =
-                    new MultiGetRequest.Item(randomAlphaOfLength(4), randomAlphaOfLength(4), randomAlphaOfLength(4));
+            MultiGetRequest.Item item = new MultiGetRequest.Item(randomAlphaOfLength(4), randomAlphaOfLength(4), randomAlphaOfLength(4));
             if (randomBoolean()) {
                 item.routing(randomAlphaOfLength(4));
             }
@@ -268,7 +270,7 @@ public class RequestConvertersTests extends ESTestCase {
 
     public void testIndicesExistEmptyIndices() {
         expectThrows(IllegalArgumentException.class, () -> RequestConverters.indicesExist(new GetIndexRequest()));
-        expectThrows(IllegalArgumentException.class, () -> RequestConverters.indicesExist(new GetIndexRequest().indices((String[])null)));
+        expectThrows(IllegalArgumentException.class, () -> RequestConverters.indicesExist(new GetIndexRequest().indices((String[]) null)));
     }
 
     private static void getAndExistsTest(Function<GetRequest, Request> requestConverter, String method) {
@@ -422,7 +424,8 @@ public class RequestConvertersTests extends ESTestCase {
         setRandomLocal(getSettingsRequest, expectedParams);
 
         if (randomBoolean()) {
-            //the request object will not have include_defaults present unless it is set to true
+            // the request object will not have include_defaults present unless it is set to
+            // true
             getSettingsRequest.includeDefaults(randomBoolean());
             if (getSettingsRequest.includeDefaults()) {
                 expectedParams.put("include_defaults", Boolean.toString(true));
@@ -966,22 +969,21 @@ public class RequestConvertersTests extends ESTestCase {
             bulkRequest.add(new IndexRequest("index", "type", "0").source(singletonMap("field", "value"), XContentType.SMILE));
             bulkRequest.add(new IndexRequest("index", "type", "1").source(singletonMap("field", "value"), XContentType.JSON));
             IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> RequestConverters.bulk(bulkRequest));
-            assertEquals("Mismatching content-type found for request with content-type [JSON], " +
-                            "previous requests have content-type [SMILE]", exception.getMessage());
+            assertEquals(
+                    "Mismatching content-type found for request with content-type [JSON], " + "previous requests have content-type [SMILE]",
+                    exception.getMessage());
         }
         {
             BulkRequest bulkRequest = new BulkRequest();
-            bulkRequest.add(new IndexRequest("index", "type", "0")
-                    .source(singletonMap("field", "value"), XContentType.JSON));
-            bulkRequest.add(new IndexRequest("index", "type", "1")
-                    .source(singletonMap("field", "value"), XContentType.JSON));
+            bulkRequest.add(new IndexRequest("index", "type", "0").source(singletonMap("field", "value"), XContentType.JSON));
+            bulkRequest.add(new IndexRequest("index", "type", "1").source(singletonMap("field", "value"), XContentType.JSON));
             bulkRequest.add(new UpdateRequest("index", "type", "2")
                     .doc(new IndexRequest().source(singletonMap("field", "value"), XContentType.JSON))
-                    .upsert(new IndexRequest().source(singletonMap("field", "value"), XContentType.SMILE))
-            );
+                    .upsert(new IndexRequest().source(singletonMap("field", "value"), XContentType.SMILE)));
             IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> RequestConverters.bulk(bulkRequest));
-            assertEquals("Mismatching content-type found for request with content-type [SMILE], " +
-                            "previous requests have content-type [JSON]", exception.getMessage());
+            assertEquals(
+                    "Mismatching content-type found for request with content-type [SMILE], " + "previous requests have content-type [JSON]",
+                    exception.getMessage());
         }
         {
             XContentType xContentType = randomFrom(XContentType.CBOR, XContentType.YAML);
@@ -1022,9 +1024,10 @@ public class RequestConvertersTests extends ESTestCase {
         setRandomIndicesOptions(searchRequest::indicesOptions, searchRequest::indicesOptions, expectedParams);
 
         SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
-        //rarely skip setting the search source completely
+        // rarely skip setting the search source completely
         if (frequently()) {
-            //frequently set the search source to have some content, otherwise leave it empty but still set it
+            // frequently set the search source to have some content, otherwise leave it
+            // empty but still set it
             if (frequently()) {
                 if (randomBoolean()) {
                     searchSourceBuilder.size(randomIntBetween(0, Integer.MAX_VALUE));
@@ -1094,7 +1097,8 @@ public class RequestConvertersTests extends ESTestCase {
         MultiSearchRequest multiSearchRequest = new MultiSearchRequest();
         for (int i = 0; i < numberOfSearchRequests; i++) {
             SearchRequest searchRequest = randomSearchRequest(() -> {
-                // No need to return a very complex SearchSourceBuilder here, that is tested elsewhere
+                // No need to return a very complex SearchSourceBuilder here, that is tested
+                // elsewhere
                 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
                 searchSourceBuilder.from(randomInt(10));
                 searchSourceBuilder.size(randomIntBetween(20, 100));
@@ -1102,14 +1106,13 @@ public class RequestConvertersTests extends ESTestCase {
             });
             // scroll is not supported in the current msearch api, so unset it:
             searchRequest.scroll((Scroll) null);
-            // only expand_wildcards, ignore_unavailable and allow_no_indices can be specified from msearch api, so unset other options:
+            // only expand_wildcards, ignore_unavailable and allow_no_indices can be
+            // specified from msearch api, so unset other options:
             IndicesOptions randomlyGenerated = searchRequest.indicesOptions();
             IndicesOptions msearchDefault = new MultiSearchRequest().indicesOptions();
-            searchRequest.indicesOptions(IndicesOptions.fromOptions(
-                    randomlyGenerated.ignoreUnavailable(), randomlyGenerated.allowNoIndices(), randomlyGenerated.expandWildcardsOpen(),
-                    randomlyGenerated.expandWildcardsClosed(), msearchDefault.allowAliasesToMultipleIndices(),
-                    msearchDefault.forbidClosedIndices(), msearchDefault.ignoreAliases()
-            ));
+            searchRequest.indicesOptions(IndicesOptions.fromOptions(randomlyGenerated.ignoreUnavailable(),
+                    randomlyGenerated.allowNoIndices(), randomlyGenerated.expandWildcardsOpen(), randomlyGenerated.expandWildcardsClosed(),
+                    msearchDefault.allowAliasesToMultipleIndices(), msearchDefault.forbidClosedIndices(), msearchDefault.ignoreAliases()));
             multiSearchRequest.add(searchRequest);
         }
 
@@ -1134,8 +1137,8 @@ public class RequestConvertersTests extends ESTestCase {
             requests.add(searchRequest);
         };
         MultiSearchRequest.readMultiLineFormat(new BytesArray(EntityUtils.toByteArray(request.getEntity())),
-                REQUEST_BODY_CONTENT_TYPE.xContent(), consumer, null, multiSearchRequest.indicesOptions(), null, null,
-                null, xContentRegistry(), true);
+                REQUEST_BODY_CONTENT_TYPE.xContent(), consumer, null, multiSearchRequest.indicesOptions(), null, null, null,
+                xContentRegistry(), true);
         assertEquals(requests, multiSearchRequest.requests());
     }
 
@@ -1230,7 +1233,7 @@ public class RequestConvertersTests extends ESTestCase {
         GetAliasesRequest getAliasesRequest = new GetAliasesRequest();
         String[] indices = randomBoolean() ? null : randomIndicesNames(0, 5);
         getAliasesRequest.indices(indices);
-        //the HEAD endpoint requires at least an alias or an index
+        // the HEAD endpoint requires at least an alias or an index
         boolean hasIndices = indices != null && indices.length > 0;
         String[] aliases;
         if (hasIndices) {
@@ -1261,15 +1264,15 @@ public class RequestConvertersTests extends ESTestCase {
     public void testExistsAliasNoAliasNoIndex() {
         {
             GetAliasesRequest getAliasesRequest = new GetAliasesRequest();
-            IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () ->
-                    RequestConverters.existsAlias(getAliasesRequest));
+            IllegalArgumentException iae = expectThrows(IllegalArgumentException.class,
+                    () -> RequestConverters.existsAlias(getAliasesRequest));
             assertEquals("existsAlias requires at least an alias or an index", iae.getMessage());
         }
         {
-            GetAliasesRequest getAliasesRequest = new GetAliasesRequest((String[])null);
-            getAliasesRequest.indices((String[])null);
-            IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () ->
-                    RequestConverters.existsAlias(getAliasesRequest));
+            GetAliasesRequest getAliasesRequest = new GetAliasesRequest((String[]) null);
+            getAliasesRequest.indices((String[]) null);
+            IllegalArgumentException iae = expectThrows(IllegalArgumentException.class,
+                    () -> RequestConverters.existsAlias(getAliasesRequest));
             assertEquals("existsAlias requires at least an alias or an index", iae.getMessage());
         }
     }
@@ -1279,14 +1282,10 @@ public class RequestConvertersTests extends ESTestCase {
         String[] indices = randomIndicesNames(0, 5);
         String[] fields = generateRandomStringArray(5, 10, false, false);
 
-        FieldCapabilitiesRequest fieldCapabilitiesRequest = new FieldCapabilitiesRequest()
-            .indices(indices)
-            .fields(fields);
+        FieldCapabilitiesRequest fieldCapabilitiesRequest = new FieldCapabilitiesRequest().indices(indices).fields(fields);
 
         Map<String, String> indicesOptionsParams = new HashMap<>();
-        setRandomIndicesOptions(fieldCapabilitiesRequest::indicesOptions,
-            fieldCapabilitiesRequest::indicesOptions,
-            indicesOptionsParams);
+        setRandomIndicesOptions(fieldCapabilitiesRequest::indicesOptions, fieldCapabilitiesRequest::indicesOptions, indicesOptionsParams);
 
         Request request = RequestConverters.fieldCaps(fieldCapabilitiesRequest);
 
@@ -1301,12 +1300,13 @@ public class RequestConvertersTests extends ESTestCase {
         assertEquals(endpoint.toString(), request.getEndpoint());
         assertEquals(4, request.getParameters().size());
 
-        // Note that we don't check the field param value explicitly, as field names are passed through
-        // a hash set before being added to the request, and can appear in a non-deterministic order.
+        // Note that we don't check the field param value explicitly, as field names are
+        // passed through
+        // a hash set before being added to the request, and can appear in a
+        // non-deterministic order.
         assertThat(request.getParameters(), hasKey("fields"));
         String[] requestFields = Strings.splitStringByCommaToArray(request.getParameters().get("fields"));
-        assertEquals(new HashSet<>(Arrays.asList(fields)),
-            new HashSet<>(Arrays.asList(requestFields)));
+        assertEquals(new HashSet<>(Arrays.asList(fields)), new HashSet<>(Arrays.asList(requestFields)));
 
         for (Map.Entry<String, String> param : indicesOptionsParams.entrySet()) {
             assertThat(request.getParameters(), hasEntry(param.getKey(), param.getValue()));
@@ -1465,6 +1465,66 @@ public class RequestConvertersTests extends ESTestCase {
         assertEquals(expectedParams, request.getParameters());
     }
 
+    public void testListTasks() {
+        {
+            ListTasksRequest request = new ListTasksRequest();
+            Map<String, String> expectedParams = new HashMap<>();
+            if (randomBoolean()) {
+                request.setDetailed(randomBoolean());
+                if (request.getDetailed()) {
+                    expectedParams.put("detailed", "true");
+                }
+            }
+            if (randomBoolean()) {
+                request.setWaitForCompletion(randomBoolean());
+                if (request.getWaitForCompletion()) {
+                    expectedParams.put("wait_for_completion", "true");
+                }
+            }
+            if (randomBoolean()) {
+                String timeout = randomTimeValue();
+                request.setTimeout(timeout);
+                expectedParams.put("timeout", timeout);
+            }
+            if (randomBoolean()) {
+                if (randomBoolean()) {
+                    TaskId taskId = new TaskId(randomAlphaOfLength(5), randomNonNegativeLong());
+                    request.setParentTaskId(taskId);
+                    expectedParams.put("parent_task_id", taskId.toString());
+                } else {
+                    request.setParentTask(TaskId.EMPTY_TASK_ID);
+                }
+            }
+            if (randomBoolean()) {
+                String[] nodes = generateRandomStringArray(10, 8, false);
+                request.setNodes(nodes);
+                if (nodes.length > 0) {
+                    expectedParams.put("nodes", String.join(",", nodes));
+                }
+            }
+            if (randomBoolean()) {
+                String[] actions = generateRandomStringArray(10, 8, false);
+                request.setActions(actions);
+                if (actions.length > 0) {
+                    expectedParams.put("actions", String.join(",", actions));
+                }
+            }
+            expectedParams.put("group_by", "none");
+            Request httpRequest = RequestConverters.listTasks(request);
+            assertThat(httpRequest, notNullValue());
+            assertThat(httpRequest.getMethod(), equalTo(HttpGet.METHOD_NAME));
+            assertThat(httpRequest.getEntity(), nullValue());
+            assertThat(httpRequest.getEndpoint(), equalTo("/_tasks"));
+            assertThat(httpRequest.getParameters(), equalTo(expectedParams));
+        }
+        {
+            ListTasksRequest request = new ListTasksRequest();
+            request.setTaskId(new TaskId(randomAlphaOfLength(5), randomNonNegativeLong()));
+            IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> RequestConverters.listTasks(request));
+            assertEquals("TaskId cannot be used for list tasks request", exception.getMessage());
+        }
+    }
+
     public void testGetRepositories() {
         Map<String, String> expectedParams = new HashMap<>();
         StringBuilder endpoint = new StringBuilder("/_snapshot");
@@ -1474,7 +1534,7 @@ public class RequestConvertersTests extends ESTestCase {
         setRandomLocal(getRepositoriesRequest, expectedParams);
 
         if (randomBoolean()) {
-            String[] entries = new String[] {"a", "b", "c"};
+            String[] entries = new String[] { "a", "b", "c" };
             getRepositoriesRequest.repositories(entries);
             endpoint.append("/" + String.join(",", entries));
         }
@@ -1513,9 +1573,8 @@ public class RequestConvertersTests extends ESTestCase {
         names.put("-#template", "-%23template");
         names.put("foo^bar", "foo%5Ebar");
 
-        PutIndexTemplateRequest putTemplateRequest = new PutIndexTemplateRequest()
-            .name(randomFrom(names.keySet()))
-            .patterns(Arrays.asList(generateRandomStringArray(20, 100, false, false)));
+        PutIndexTemplateRequest putTemplateRequest = new PutIndexTemplateRequest().name(randomFrom(names.keySet()))
+                .patterns(Arrays.asList(generateRandomStringArray(20, 100, false, false)));
         if (randomBoolean()) {
             putTemplateRequest.order(randomInt());
         }
@@ -1572,14 +1631,12 @@ public class RequestConvertersTests extends ESTestCase {
             assertEquals("/a/b", endpointBuilder.build());
         }
         {
-            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("a").addPathPart("b")
-                    .addPathPartAsIs("_create");
+            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("a").addPathPart("b").addPathPartAsIs("_create");
             assertEquals("/a/b/_create", endpointBuilder.build());
         }
 
         {
-            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("a", "b", "c")
-                    .addPathPartAsIs("_create");
+            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("a", "b", "c").addPathPartAsIs("_create");
             assertEquals("/a/b/c/_create", endpointBuilder.build());
         }
         {
@@ -1638,13 +1695,12 @@ public class RequestConvertersTests extends ESTestCase {
             assertEquals("/foo%5Ebar", endpointBuilder.build());
         }
         {
-            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("cluster1:index1,index2")
-                    .addPathPartAsIs("_search");
+            EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPart("cluster1:index1,index2").addPathPartAsIs("_search");
             assertEquals("/cluster1:index1,index2/_search", endpointBuilder.build());
         }
         {
-            EndpointBuilder endpointBuilder = new EndpointBuilder()
-                    .addCommaSeparatedPathParts(new String[]{"index1", "index2"}).addPathPartAsIs("cache/clear");
+            EndpointBuilder endpointBuilder = new EndpointBuilder().addCommaSeparatedPathParts(new String[] { "index1", "index2" })
+                    .addPathPartAsIs("cache/clear");
             assertEquals("/index1,index2/cache/clear", endpointBuilder.build());
         }
     }
@@ -1652,12 +1708,12 @@ public class RequestConvertersTests extends ESTestCase {
     public void testEndpoint() {
         assertEquals("/index/type/id", RequestConverters.endpoint("index", "type", "id"));
         assertEquals("/index/type/id/_endpoint", RequestConverters.endpoint("index", "type", "id", "_endpoint"));
-        assertEquals("/index1,index2", RequestConverters.endpoint(new String[]{"index1", "index2"}));
-        assertEquals("/index1,index2/_endpoint", RequestConverters.endpoint(new String[]{"index1", "index2"}, "_endpoint"));
-        assertEquals("/index1,index2/type1,type2/_endpoint", RequestConverters.endpoint(new String[]{"index1", "index2"},
-                new String[]{"type1", "type2"}, "_endpoint"));
-        assertEquals("/index1,index2/_endpoint/suffix1,suffix2", RequestConverters.endpoint(new String[]{"index1", "index2"},
-                "_endpoint", new String[]{"suffix1", "suffix2"}));
+        assertEquals("/index1,index2", RequestConverters.endpoint(new String[] { "index1", "index2" }));
+        assertEquals("/index1,index2/_endpoint", RequestConverters.endpoint(new String[] { "index1", "index2" }, "_endpoint"));
+        assertEquals("/index1,index2/type1,type2/_endpoint",
+                RequestConverters.endpoint(new String[] { "index1", "index2" }, new String[] { "type1", "type2" }, "_endpoint"));
+        assertEquals("/index1,index2/_endpoint/suffix1,suffix2",
+                RequestConverters.endpoint(new String[] { "index1", "index2" }, "_endpoint", new String[] { "suffix1", "suffix2" }));
     }
 
     public void testCreateContentType() {
@@ -1673,20 +1729,22 @@ public class RequestConvertersTests extends ESTestCase {
 
         XContentType bulkContentType = randomBoolean() ? xContentType : null;
 
-        IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () ->
-                enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), XContentType.CBOR), bulkContentType));
+        IllegalArgumentException exception = expectThrows(IllegalArgumentException.class,
+                () -> enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), XContentType.CBOR),
+                        bulkContentType));
         assertEquals("Unsupported content-type found for request with content-type [CBOR], only JSON and SMILE are supported",
                 exception.getMessage());
 
-        exception = expectThrows(IllegalArgumentException.class, () ->
-                enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), XContentType.YAML), bulkContentType));
+        exception = expectThrows(IllegalArgumentException.class,
+                () -> enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), XContentType.YAML),
+                        bulkContentType));
         assertEquals("Unsupported content-type found for request with content-type [YAML], only JSON and SMILE are supported",
                 exception.getMessage());
 
         XContentType requestContentType = xContentType == XContentType.JSON ? XContentType.SMILE : XContentType.JSON;
 
-        exception = expectThrows(IllegalArgumentException.class, () ->
-                enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), requestContentType), xContentType));
+        exception = expectThrows(IllegalArgumentException.class,
+                () -> enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), requestContentType), xContentType));
         assertEquals("Mismatching content-type found for request with content-type [" + requestContentType + "], "
                 + "previous requests have content-type [" + xContentType + "]", exception.getMessage());
     }
@@ -1754,11 +1812,10 @@ public class RequestConvertersTests extends ESTestCase {
     }
 
     private static void setRandomIndicesOptions(Consumer<IndicesOptions> setter, Supplier<IndicesOptions> getter,
-                                                Map<String, String> expectedParams) {
+            Map<String, String> expectedParams) {
 
         if (randomBoolean()) {
-            setter.accept(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), randomBoolean(),
-                randomBoolean()));
+            setter.accept(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean()));
         }
         expectedParams.put("ignore_unavailable", Boolean.toString(getter.get().ignoreUnavailable()));
         expectedParams.put("allow_no_indices", Boolean.toString(getter.get().allowNoIndices()));

+ 95 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/ClusterClientDocumentationIT.java

@@ -19,8 +19,14 @@
 
 package org.elasticsearch.client.documentation;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.FailedNodeException;
 import org.elasticsearch.action.LatchedActionListener;
+import org.elasticsearch.action.TaskOperationFailure;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
+import org.elasticsearch.action.admin.cluster.node.tasks.list.TaskGroup;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
@@ -31,14 +37,20 @@ import org.elasticsearch.common.unit.ByteSizeUnit;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.indices.recovery.RecoverySettings;
+import org.elasticsearch.tasks.TaskId;
+import org.elasticsearch.tasks.TaskInfo;
 
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+import static java.util.Collections.emptyList;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.notNullValue;
 
 /**
  * This class is used to generate the Java Cluster API documentation.
@@ -177,4 +189,87 @@ public class ClusterClientDocumentationIT extends ESRestHighLevelClientTestCase
             assertTrue(latch.await(30L, TimeUnit.SECONDS));
         }
     }
+
+    public void testListTasks() throws IOException {
+        RestHighLevelClient client = highLevelClient();
+        {
+            // tag::list-tasks-request
+            ListTasksRequest request = new ListTasksRequest();
+            // end::list-tasks-request
+
+            // tag::list-tasks-request-filter
+            request.setActions("cluster:*"); // <1>
+            request.setNodes("nodeId1", "nodeId2"); // <2>
+            request.setParentTaskId(new TaskId("parentTaskId", 42)); // <3>
+            // end::list-tasks-request-filter
+
+            // tag::list-tasks-request-detailed
+            request.setDetailed(true); // <1>
+            // end::list-tasks-request-detailed
+
+            // tag::list-tasks-request-wait-completion
+            request.setWaitForCompletion(true); // <1>
+            request.setTimeout(TimeValue.timeValueSeconds(50)); // <2>
+            request.setTimeout("50s"); // <3>
+            // end::list-tasks-request-wait-completion
+        }
+
+        ListTasksRequest request = new ListTasksRequest();
+
+        // tag::list-tasks-execute
+        ListTasksResponse response = client.cluster().listTasks(request);
+        // end::list-tasks-execute
+
+        assertThat(response, notNullValue());
+
+        // tag::list-tasks-response-tasks
+        List<TaskInfo> tasks = response.getTasks(); // <1>
+        // end::list-tasks-response-tasks
+
+        // tag::list-tasks-response-calc
+        Map<String, List<TaskInfo>> perNodeTasks = response.getPerNodeTasks(); // <1>
+        List<TaskGroup> groups = response.getTaskGroups(); // <2>
+        // end::list-tasks-response-calc
+
+        // tag::list-tasks-response-failures
+        List<ElasticsearchException> nodeFailures = response.getNodeFailures(); // <1>
+        List<TaskOperationFailure> taskFailures = response.getTaskFailures(); // <2>
+        // end::list-tasks-response-failures
+
+        assertThat(response.getNodeFailures(), equalTo(emptyList()));
+        assertThat(response.getTaskFailures(), equalTo(emptyList()));
+        assertThat(response.getTasks().size(), greaterThanOrEqualTo(2));
+    }
+
+    public void testListTasksAsync() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+        {
+            ListTasksRequest request = new ListTasksRequest();
+
+            // tag::list-tasks-execute-listener
+            ActionListener<ListTasksResponse> listener =
+                    new ActionListener<ListTasksResponse>() {
+                        @Override
+                        public void onResponse(ListTasksResponse response) {
+                            // <1>
+                        }
+
+                        @Override
+                        public void onFailure(Exception e) {
+                            // <2>
+                        }
+                    };
+            // end::list-tasks-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::list-tasks-execute-async
+            client.cluster().listTasksAsync(request, listener); // <1>
+            // end::list-tasks-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
 }

+ 101 - 0
docs/java-rest/high-level/cluster/list_tasks.asciidoc

@@ -0,0 +1,101 @@
+[[java-rest-high-cluster-list-tasks]]
+=== List Tasks API
+
+The List Tasks API allows to get information about the tasks currently executing in the cluster.
+
+[[java-rest-high-cluster-list-tasks-request]]
+==== List Tasks Request
+
+A `ListTasksRequest`:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-request]
+--------------------------------------------------
+There is no required parameters. By default the client will list all tasks and will not wait
+for task completion.
+
+==== Parameters
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-request-filter]
+--------------------------------------------------
+<1> Request only cluster-related tasks
+<2> Request all tasks running on nodes nodeId1 and nodeId2
+<3> Request only children of a particular task
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-request-detailed]
+--------------------------------------------------
+<1> Should the information include detailed, potentially slow to generate data. Defaults to `false`
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-request-wait-completion]
+--------------------------------------------------
+<1> Should this request wait for all found tasks to complete. Defaults to `false`
+<2> Timeout for the request as a `TimeValue`. Applicable only if `setWaitForCompletion` is `true`.
+Defaults to 30 seconds
+<3> Timeout as a `String`
+
+[[java-rest-high-cluster-list-tasks-sync]]
+==== Synchronous Execution
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-execute]
+--------------------------------------------------
+
+[[java-rest-high-cluster-list-tasks-async]]
+==== Asynchronous Execution
+
+The asynchronous execution of a cluster update settings requires both the
+`ListTasksRequest` instance and an `ActionListener` instance to be
+passed to the asynchronous method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-execute-async]
+--------------------------------------------------
+<1> The `ListTasksRequest` 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 `ListTasksResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-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-cluster-list-tasks-response]]
+==== List Tasks Response
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-response-tasks]
+--------------------------------------------------
+<1> List of currently running tasks
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-response-calc]
+--------------------------------------------------
+<1> List of tasks grouped by a node
+<2> List of tasks grouped by a parent task
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/ClusterClientDocumentationIT.java[list-tasks-response-failures]
+--------------------------------------------------
+<1> List of node failures
+<2> List of tasks failures

+ 3 - 1
docs/java-rest/high-level/supported-apis.asciidoc

@@ -104,8 +104,10 @@ include::indices/put_template.asciidoc[]
 The Java High Level REST Client supports the following Cluster APIs:
 
 * <<java-rest-high-cluster-put-settings>>
+* <<java-rest-high-cluster-list-tasks>>
 
 include::cluster/put_settings.asciidoc[]
+include::cluster/list_tasks.asciidoc[]
 
 == Snapshot APIs
 
@@ -114,4 +116,4 @@ The Java High Level REST Client supports the following Snapshot APIs:
 * <<java-rest-high-snapshot-get-repository>>
 
 include::snapshot/get_repository.asciidoc[]
-include::snapshot/create_repository.asciidoc[]
+include::snapshot/create_repository.asciidoc[]

+ 31 - 7
server/src/main/java/org/elasticsearch/action/TaskOperationFailure.java

@@ -21,17 +21,20 @@ package org.elasticsearch.action;
 
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
-import org.elasticsearch.common.xcontent.ToXContent.Params;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentFragment;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
 
 import static org.elasticsearch.ExceptionsHelper.detailedMessage;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
 
 /**
  * Information about task operation failures
@@ -39,7 +42,10 @@ import static org.elasticsearch.ExceptionsHelper.detailedMessage;
  * The class is final due to serialization limitations
  */
 public final class TaskOperationFailure implements Writeable, ToXContentFragment {
-
+    private static final String TASK_ID = "task_id";
+    private static final String NODE_ID = "node_id";
+    private static final String STATUS = "status";
+    private static final String REASON = "reason";
     private final String nodeId;
 
     private final long taskId;
@@ -48,6 +54,21 @@ public final class TaskOperationFailure implements Writeable, ToXContentFragment
 
     private final RestStatus status;
 
+    private static final ConstructingObjectParser<TaskOperationFailure, Void> PARSER =
+            new ConstructingObjectParser<>("task_info", true, constructorObjects -> {
+                int i = 0;
+                String nodeId = (String) constructorObjects[i++];
+                long taskId = (long) constructorObjects[i++];
+                ElasticsearchException reason = (ElasticsearchException) constructorObjects[i];
+                return new TaskOperationFailure(nodeId, taskId, reason);
+            });
+
+    static {
+        PARSER.declareString(constructorArg(), new ParseField(NODE_ID));
+        PARSER.declareLong(constructorArg(), new ParseField(TASK_ID));
+        PARSER.declareObject(constructorArg(), (parser, c) -> ElasticsearchException.fromXContent(parser), new ParseField(REASON));
+    }
+
     public TaskOperationFailure(String nodeId, long taskId, Exception e) {
         this.nodeId = nodeId;
         this.taskId = taskId;
@@ -98,13 +119,17 @@ public final class TaskOperationFailure implements Writeable, ToXContentFragment
         return "[" + nodeId + "][" + taskId + "] failed, reason [" + getReason() + "]";
     }
 
+    public static TaskOperationFailure fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.field("task_id", getTaskId());
-        builder.field("node_id", getNodeId());
-        builder.field("status", status.name());
+        builder.field(TASK_ID, getTaskId());
+        builder.field(NODE_ID, getNodeId());
+        builder.field(STATUS, status.name());
         if (reason != null) {
-            builder.field("reason");
+            builder.field(REASON);
             builder.startObject();
             ElasticsearchException.generateThrowableXContent(builder, params, reason);
             builder.endObject();
@@ -112,5 +137,4 @@ public final class TaskOperationFailure implements Writeable, ToXContentFragment
         return builder;
 
     }
-
 }

+ 42 - 9
server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java

@@ -19,16 +19,19 @@
 
 package org.elasticsearch.action.admin.cluster.node.tasks.list;
 
-import org.elasticsearch.action.FailedNodeException;
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.TaskOperationFailure;
 import org.elasticsearch.action.support.tasks.BaseTasksResponse;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
 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 org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.tasks.TaskInfo;
 
@@ -40,10 +43,16 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
 /**
  * Returns the list of tasks currently running on the nodes
  */
 public class ListTasksResponse extends BaseTasksResponse implements ToXContentObject {
+    private static final String TASKS = "tasks";
+    private static final String TASK_FAILURES = "task_failures";
+    private static final String NODE_FAILURES = "node_failures";
 
     private List<TaskInfo> tasks;
 
@@ -56,11 +65,31 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
     }
 
     public ListTasksResponse(List<TaskInfo> tasks, List<TaskOperationFailure> taskFailures,
-            List<? extends FailedNodeException> nodeFailures) {
+            List<? extends ElasticsearchException> nodeFailures) {
         super(taskFailures, nodeFailures);
         this.tasks = tasks == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(tasks));
     }
 
+    private static final ConstructingObjectParser<ListTasksResponse, Void> PARSER =
+        new ConstructingObjectParser<>("list_tasks_response", true,
+            constructingObjects -> {
+                int i = 0;
+                @SuppressWarnings("unchecked")
+                List<TaskInfo> tasks = (List<TaskInfo>) constructingObjects[i++];
+                @SuppressWarnings("unchecked")
+                List<TaskOperationFailure> tasksFailures = (List<TaskOperationFailure>) constructingObjects[i++];
+                @SuppressWarnings("unchecked")
+                List<ElasticsearchException> nodeFailures = (List<ElasticsearchException>) constructingObjects[i];
+                return new ListTasksResponse(tasks, tasksFailures, nodeFailures);
+            });
+
+    static {
+        PARSER.declareObjectArray(constructorArg(), TaskInfo.PARSER, new ParseField(TASKS));
+        PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> TaskOperationFailure.fromXContent(p), new ParseField(TASK_FAILURES));
+        PARSER.declareObjectArray(optionalConstructorArg(),
+            (parser, c) -> ElasticsearchException.fromXContent(parser), new ParseField(NODE_FAILURES));
+    }
+
     @Override
     public void readFrom(StreamInput in) throws IOException {
         super.readFrom(in);
@@ -159,7 +188,7 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
                     builder.endObject();
                 }
             }
-            builder.startObject("tasks");
+            builder.startObject(TASKS);
             for(TaskInfo task : entry.getValue()) {
                 builder.startObject(task.getTaskId().toString());
                 task.toXContent(builder, params);
@@ -177,7 +206,7 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
      */
     public XContentBuilder toXContentGroupedByParents(XContentBuilder builder, Params params) throws IOException {
         toXContentCommon(builder, params);
-        builder.startObject("tasks");
+        builder.startObject(TASKS);
         for (TaskGroup group : getTaskGroups()) {
             builder.field(group.getTaskInfo().getTaskId().toString());
             group.toXContent(builder, params);
@@ -191,7 +220,7 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
      */
     public XContentBuilder toXContentGroupedByNone(XContentBuilder builder, Params params) throws IOException {
         toXContentCommon(builder, params);
-        builder.startArray("tasks");
+        builder.startArray(TASKS);
         for (TaskInfo taskInfo : getTasks()) {
             builder.startObject();
             taskInfo.toXContent(builder, params);
@@ -204,14 +233,14 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
-        toXContentGroupedByParents(builder, params);
+        toXContentGroupedByNone(builder, params);
         builder.endObject();
         return builder;
     }
 
     private void toXContentCommon(XContentBuilder builder, Params params) throws IOException {
         if (getTaskFailures() != null && getTaskFailures().size() > 0) {
-            builder.startArray("task_failures");
+            builder.startArray(TASK_FAILURES);
             for (TaskOperationFailure ex : getTaskFailures()){
                 builder.startObject();
                 builder.value(ex);
@@ -221,8 +250,8 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
         }
 
         if (getNodeFailures() != null && getNodeFailures().size() > 0) {
-            builder.startArray("node_failures");
-            for (FailedNodeException ex : getNodeFailures()) {
+            builder.startArray(NODE_FAILURES);
+            for (ElasticsearchException ex : getNodeFailures()) {
                 builder.startObject();
                 ex.toXContent(builder, params);
                 builder.endObject();
@@ -231,6 +260,10 @@ public class ListTasksResponse extends BaseTasksResponse implements ToXContentOb
         }
     }
 
+    public static ListTasksResponse fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
     @Override
     public String toString() {
         return Strings.toString(this);

+ 4 - 4
server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksResponse.java

@@ -42,9 +42,9 @@ import static org.elasticsearch.ExceptionsHelper.rethrowAndSuppress;
  */
 public class BaseTasksResponse extends ActionResponse {
     private List<TaskOperationFailure> taskFailures;
-    private List<FailedNodeException> nodeFailures;
+    private List<ElasticsearchException> nodeFailures;
 
-    public BaseTasksResponse(List<TaskOperationFailure> taskFailures, List<? extends FailedNodeException> nodeFailures) {
+    public BaseTasksResponse(List<TaskOperationFailure> taskFailures, List<? extends ElasticsearchException> nodeFailures) {
         this.taskFailures = taskFailures == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(taskFailures));
         this.nodeFailures = nodeFailures == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(nodeFailures));
     }
@@ -59,7 +59,7 @@ public class BaseTasksResponse extends ActionResponse {
     /**
      * The list of node failures exception.
      */
-    public List<FailedNodeException> getNodeFailures() {
+    public List<ElasticsearchException> getNodeFailures() {
         return nodeFailures;
     }
 
@@ -99,7 +99,7 @@ public class BaseTasksResponse extends ActionResponse {
             exp.writeTo(out);
         }
         out.writeVInt(nodeFailures.size());
-        for (FailedNodeException exp : nodeFailures) {
+        for (ElasticsearchException exp : nodeFailures) {
             exp.writeTo(out);
         }
     }

+ 3 - 4
server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java

@@ -103,18 +103,17 @@ public class RestListTasksAction extends BaseRestHandler {
                     return new BytesRestResponse(RestStatus.OK, builder);
                 }
             };
-        } else if ("none".equals(groupBy)) {
+        } else if ("parents".equals(groupBy)) {
             return new RestBuilderListener<T>(channel) {
                 @Override
                 public RestResponse buildResponse(T response, XContentBuilder builder) throws Exception {
                     builder.startObject();
-                    response.toXContentGroupedByNone(builder, channel.request());
+                    response.toXContentGroupedByParents(builder, channel.request());
                     builder.endObject();
                     return new BytesRestResponse(RestStatus.OK, builder);
                 }
             };
-
-        } else if ("parents".equals(groupBy)) {
+        } else if ("none".equals(groupBy)) {
             return new RestToXContentListener<>(channel);
         } else {
             throw new IllegalArgumentException("[group_by] must be one of [nodes], [parents] or [none] but was [" + groupBy + "]");

+ 5 - 0
server/src/main/java/org/elasticsearch/tasks/TaskInfo.java

@@ -32,6 +32,7 @@ import org.elasticsearch.common.xcontent.ObjectParserHelper;
 import org.elasticsearch.common.xcontent.ToXContent.Params;
 import org.elasticsearch.common.xcontent.ToXContentFragment;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -214,6 +215,10 @@ public final class TaskInfo implements Writeable, ToXContentFragment {
         return builder;
     }
 
+    public static TaskInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
     public static final ConstructingObjectParser<TaskInfo, Void> PARSER = new ConstructingObjectParser<>(
             "task_info", true, a -> {
                 int i = 0;

+ 63 - 0
server/src/test/java/org/elasticsearch/action/TaskOperationFailureTests.java

@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class TaskOperationFailureTests extends AbstractXContentTestCase<TaskOperationFailure> {
+
+    @Override
+    protected TaskOperationFailure createTestInstance() {
+        return new TaskOperationFailure(randomAlphaOfLength(5), randomNonNegativeLong(), new IllegalStateException("message"));
+    }
+
+    @Override
+    protected TaskOperationFailure doParseInstance(XContentParser parser) throws IOException {
+        return TaskOperationFailure.fromXContent(parser);
+    }
+
+    @Override
+    protected void assertEqualInstances(TaskOperationFailure expectedInstance, TaskOperationFailure newInstance) {
+        assertNotSame(expectedInstance, newInstance);
+        assertThat(newInstance.getNodeId(), equalTo(expectedInstance.getNodeId()));
+        assertThat(newInstance.getTaskId(), equalTo(expectedInstance.getTaskId()));
+        assertThat(newInstance.getStatus(), equalTo(expectedInstance.getStatus()));
+        // XContent loses the original exception and wraps it as a message in Elasticsearch exception
+        assertThat(newInstance.getCause().getMessage(), equalTo("Elasticsearch exception [type=illegal_state_exception, reason=message]"));
+        // getReason returns Exception class and the message
+        assertThat(newInstance.getReason(),
+            equalTo("ElasticsearchException[Elasticsearch exception [type=illegal_state_exception, reason=message]]"));
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    @Override
+    protected boolean assertToXContentEquivalence() {
+        return false;
+    }
+}

+ 2 - 1
server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java

@@ -18,6 +18,7 @@
  */
 package org.elasticsearch.action.admin.cluster.node.tasks;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ElasticsearchTimeoutException;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.ResourceNotFoundException;
@@ -716,7 +717,7 @@ public class TasksIT extends ESIntegTestCase {
             .setTimeout(timeValueSeconds(10)).get();
 
         // It should finish quickly and without complaint and list the list tasks themselves
-        assertThat(response.getNodeFailures(), emptyCollectionOf(FailedNodeException.class));
+        assertThat(response.getNodeFailures(), emptyCollectionOf(ElasticsearchException.class));
         assertThat(response.getTaskFailures(), emptyCollectionOf(TaskOperationFailure.class));
         assertThat(response.getTasks().size(), greaterThanOrEqualTo(1));
     }

+ 60 - 5
server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java

@@ -19,18 +19,33 @@
 
 package org.elasticsearch.tasks;
 
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.FailedNodeException;
+import org.elasticsearch.action.TaskOperationFailure;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
-import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.AbstractXContentTestCase;
 
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
 
-public class ListTasksResponseTests extends ESTestCase {
+public class ListTasksResponseTests extends AbstractXContentTestCase<ListTasksResponse> {
 
     public void testEmptyToString() {
-        assertEquals("{\"tasks\":{}}", new ListTasksResponse().toString());
+        assertEquals("{\"tasks\":[]}", new ListTasksResponse().toString());
     }
 
     public void testNonEmptyToString() {
@@ -38,8 +53,48 @@ public class ListTasksResponseTests extends ESTestCase {
             new TaskId("node1", 1), "dummy-type", "dummy-action", "dummy-description", null, 0, 1, true, new TaskId("node1", 0),
             Collections.singletonMap("foo", "bar"));
         ListTasksResponse tasksResponse = new ListTasksResponse(singletonList(info), emptyList(), emptyList());
-        assertEquals("{\"tasks\":{\"node1:1\":{\"node\":\"node1\",\"id\":1,\"type\":\"dummy-type\",\"action\":\"dummy-action\","
+        assertEquals("{\"tasks\":[{\"node\":\"node1\",\"id\":1,\"type\":\"dummy-type\",\"action\":\"dummy-action\","
                 + "\"description\":\"dummy-description\",\"start_time_in_millis\":0,\"running_time_in_nanos\":1,\"cancellable\":true,"
-                + "\"parent_task_id\":\"node1:0\",\"headers\":{\"foo\":\"bar\"}}}}", tasksResponse.toString());
+                + "\"parent_task_id\":\"node1:0\",\"headers\":{\"foo\":\"bar\"}}]}", tasksResponse.toString());
+    }
+
+    @Override
+    protected ListTasksResponse createTestInstance() {
+        List<TaskInfo> tasks = new ArrayList<>();
+        for (int i = 0; i < randomInt(10); i++) {
+            tasks.add(TaskInfoTests.randomTaskInfo());
+        }
+        List<TaskOperationFailure> taskFailures = new ArrayList<>();
+        for (int i = 0; i < randomInt(5); i++) {
+            taskFailures.add(new TaskOperationFailure(
+                    randomAlphaOfLength(5), randomNonNegativeLong(), new IllegalStateException("message")));
+        }
+        return new ListTasksResponse(tasks, taskFailures, Collections.singletonList(new FailedNodeException("", "message", null)));
+    }
+
+    @Override
+    protected ListTasksResponse doParseInstance(XContentParser parser) throws IOException {
+        return ListTasksResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    @Override
+    protected void assertEqualInstances(ListTasksResponse expectedInstance, ListTasksResponse newInstance) {
+        assertNotSame(expectedInstance, newInstance);
+        assertThat(newInstance.getTasks(), equalTo(expectedInstance.getTasks()));
+        assertThat(newInstance.getNodeFailures().size(), equalTo(1));
+        for (ElasticsearchException failure : newInstance.getNodeFailures()) {
+            assertThat(failure, notNullValue());
+            assertThat(failure.getMessage(), equalTo("Elasticsearch exception [type=failed_node_exception, reason=message]"));
+        }
+    }
+
+    @Override
+    protected boolean assertToXContentEquivalence() {
+        return false;
     }
 }

+ 156 - 0
server/src/test/java/org/elasticsearch/tasks/TaskInfoTests.java

@@ -0,0 +1,156 @@
+/*
+ * 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.tasks;
+
+import org.elasticsearch.client.Requests;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractSerializingTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class TaskInfoTests extends AbstractSerializingTestCase<TaskInfo> {
+
+    @Override
+    protected TaskInfo doParseInstance(XContentParser parser) {
+        return TaskInfo.fromXContent(parser);
+    }
+
+    @Override
+    protected TaskInfo createTestInstance() {
+        return randomTaskInfo();
+    }
+
+    @Override
+    protected Writeable.Reader<TaskInfo> instanceReader() {
+        return TaskInfo::new;
+    }
+
+    @Override
+    protected NamedWriteableRegistry getNamedWriteableRegistry() {
+        return new NamedWriteableRegistry(Collections.singletonList(
+                new NamedWriteableRegistry.Entry(Task.Status.class, RawTaskStatus.NAME, RawTaskStatus::new)));
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+
+    @Override
+    protected Predicate<String> getRandomFieldsExcludeFilter() {
+        return field -> "status".equals(field) || "headers".equals(field);
+    }
+
+    @Override
+    protected TaskInfo mutateInstance(TaskInfo info) throws IOException {
+        switch (between(0, 9)) {
+            case 0:
+                TaskId taskId = new TaskId(info.getTaskId().getNodeId() + randomAlphaOfLength(5), info.getTaskId().getId());
+                return new TaskInfo(taskId, info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(), info.getHeaders());
+            case 1:
+                return new TaskInfo(info.getTaskId(), info.getType() + randomAlphaOfLength(5), info.getAction(), info.getDescription(),
+                    info.getStatus(), info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(),
+                    info.getHeaders());
+            case 2:
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction() + randomAlphaOfLength(5), info.getDescription(),
+                    info.getStatus(), info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(),
+                    info.getHeaders());
+            case 3:
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription() + randomAlphaOfLength(5),
+                    info.getStatus(), info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(),
+                    info.getHeaders());
+            case 4:
+                Task.Status newStatus = randomValueOtherThan(info.getStatus(), TaskInfoTests::randomRawTaskStatus);
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), newStatus,
+                    info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(), info.getHeaders());
+            case 5:
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime() + between(1, 100), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(),
+                    info.getHeaders());
+            case 6:
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime(), info.getRunningTimeNanos() + between(1, 100), info.isCancellable(), info.getParentTaskId(),
+                    info.getHeaders());
+            case 7:
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable() == false, info.getParentTaskId(),
+                    info.getHeaders());
+            case 8:
+                TaskId parentId = new TaskId(info.getParentTaskId().getNodeId() + randomAlphaOfLength(5), info.getParentTaskId().getId());
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), parentId, info.getHeaders());
+            case 9:
+                Map<String, String> headers = info.getHeaders();
+                if (headers == null) {
+                    headers = new HashMap<>(1);
+                } else {
+                    headers = new HashMap<>(info.getHeaders());
+                }
+                headers.put(randomAlphaOfLength(15), randomAlphaOfLength(15));
+                return new TaskInfo(info.getTaskId(), info.getType(), info.getAction(), info.getDescription(), info.getStatus(),
+                    info.getStartTime(), info.getRunningTimeNanos(), info.isCancellable(), info.getParentTaskId(), headers);
+            default:
+                throw new IllegalStateException();
+        }
+    }
+
+    static TaskInfo randomTaskInfo() {
+        TaskId taskId = randomTaskId();
+        String type = randomAlphaOfLength(5);
+        String action = randomAlphaOfLength(5);
+        Task.Status status = randomBoolean() ? randomRawTaskStatus() : null;
+        String description = randomBoolean() ? randomAlphaOfLength(5) : null;
+        long startTime = randomLong();
+        long runningTimeNanos = randomLong();
+        boolean cancellable = randomBoolean();
+        TaskId parentTaskId = randomBoolean() ? TaskId.EMPTY_TASK_ID : randomTaskId();
+        Map<String, String> headers = randomBoolean() ?
+                Collections.emptyMap() :
+                Collections.singletonMap(randomAlphaOfLength(5), randomAlphaOfLength(5));
+        return new TaskInfo(taskId, type, action, description, status, startTime, runningTimeNanos, cancellable, parentTaskId, headers);
+    }
+
+    private static TaskId randomTaskId() {
+        return new TaskId(randomAlphaOfLength(5), randomLong());
+    }
+
+    private static RawTaskStatus randomRawTaskStatus() {
+        try (XContentBuilder builder = XContentBuilder.builder(Requests.INDEX_CONTENT_TYPE.xContent())) {
+            builder.startObject();
+            int fields = between(0, 10);
+            for (int f = 0; f < fields; f++) {
+                builder.field(randomAlphaOfLength(5), randomAlphaOfLength(5));
+            }
+            builder.endObject();
+            return new RawTaskStatus(BytesReference.bytes(builder));
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}

+ 2 - 33
server/src/test/java/org/elasticsearch/tasks/TaskResultTests.java

@@ -19,8 +19,6 @@
 
 package org.elasticsearch.tasks;
 
-import org.elasticsearch.client.Requests;
-import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
@@ -37,6 +35,8 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
 
+import static org.elasticsearch.tasks.TaskInfoTests.randomTaskInfo;
+
 /**
  * Round trip tests for {@link TaskResult} and those classes that it includes like {@link TaskInfo} and {@link RawTaskStatus}.
  */
@@ -125,37 +125,6 @@ public class TaskResultTests extends ESTestCase {
         }
     }
 
-    private static TaskInfo randomTaskInfo() throws IOException {
-        TaskId taskId = randomTaskId();
-        String type = randomAlphaOfLength(5);
-        String action = randomAlphaOfLength(5);
-        Task.Status status = randomBoolean() ? randomRawTaskStatus() : null;
-        String description = randomBoolean() ? randomAlphaOfLength(5) : null;
-        long startTime = randomLong();
-        long runningTimeNanos = randomLong();
-        boolean cancellable = randomBoolean();
-        TaskId parentTaskId = randomBoolean() ? TaskId.EMPTY_TASK_ID : randomTaskId();
-        Map<String, String> headers =
-            randomBoolean() ? Collections.emptyMap() : Collections.singletonMap(randomAlphaOfLength(5), randomAlphaOfLength(5));
-        return new TaskInfo(taskId, type, action, description, status, startTime, runningTimeNanos, cancellable, parentTaskId, headers);
-    }
-
-    private static TaskId randomTaskId() {
-        return new TaskId(randomAlphaOfLength(5), randomLong());
-    }
-
-    private static RawTaskStatus randomRawTaskStatus() throws IOException {
-        try (XContentBuilder builder = XContentBuilder.builder(Requests.INDEX_CONTENT_TYPE.xContent())) {
-            builder.startObject();
-            int fields = between(0, 10);
-            for (int f = 0; f < fields; f++) {
-                builder.field(randomAlphaOfLength(5), randomAlphaOfLength(5));
-            }
-            builder.endObject();
-            return new RawTaskStatus(BytesReference.bytes(builder));
-        }
-    }
-
     private static ToXContent randomTaskResponse() {
         Map<String, String> result = new TreeMap<>();
         int fields = between(0, 10);

+ 4 - 4
test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

@@ -659,20 +659,20 @@ public abstract class ESTestCase extends LuceneTestCase {
         return RandomizedTest.randomRealisticUnicodeOfCodepointLength(codePoints);
     }
 
-    public static String[] generateRandomStringArray(int maxArraySize, int maxStringSize, boolean allowNull, boolean allowEmpty) {
+    public static String[] generateRandomStringArray(int maxArraySize, int stringSize, boolean allowNull, boolean allowEmpty) {
         if (allowNull && random().nextBoolean()) {
             return null;
         }
         int arraySize = randomIntBetween(allowEmpty ? 0 : 1, maxArraySize);
         String[] array = new String[arraySize];
         for (int i = 0; i < arraySize; i++) {
-            array[i] = RandomStrings.randomAsciiOfLength(random(), maxStringSize);
+            array[i] = RandomStrings.randomAsciiOfLength(random(), stringSize);
         }
         return array;
     }
 
-    public static String[] generateRandomStringArray(int maxArraySize, int maxStringSize, boolean allowNull) {
-        return generateRandomStringArray(maxArraySize, maxStringSize, allowNull, true);
+    public static String[] generateRandomStringArray(int maxArraySize, int stringSize, boolean allowNull) {
+        return generateRandomStringArray(maxArraySize, stringSize, allowNull, true);
     }
 
     private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"};

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java

@@ -5,11 +5,11 @@
  */
 package org.elasticsearch.xpack.core.ml.action;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.Version;
 import org.elasticsearch.action.Action;
 import org.elasticsearch.action.ActionRequestBuilder;
 import org.elasticsearch.action.ActionRequestValidationException;
-import org.elasticsearch.action.FailedNodeException;
 import org.elasticsearch.action.TaskOperationFailure;
 import org.elasticsearch.action.support.tasks.BaseTasksRequest;
 import org.elasticsearch.action.support.tasks.BaseTasksResponse;
@@ -297,7 +297,7 @@ public class GetJobsStatsAction extends Action<GetJobsStatsAction.Request, GetJo
             this.jobsStats = jobsStats;
         }
 
-        public Response(List<TaskOperationFailure> taskFailures, List<? extends FailedNodeException> nodeFailures,
+        public Response(List<TaskOperationFailure> taskFailures, List<? extends ElasticsearchException> nodeFailures,
                  QueryPage<JobStats> jobsStats) {
             super(taskFailures, nodeFailures);
             this.jobsStats = jobsStats;