Browse Source

Fix HLRC parsing of CancelTasks response (#47017)

Adds support for proper cancel tasks parsing.

Closes #45414
jesinity 5 years ago
parent
commit
52003a2dbb
19 changed files with 1638 additions and 52 deletions
  1. 26 3
      client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
  2. 2 2
      client/rest-high-level/src/main/java/org/elasticsearch/client/TasksClient.java
  3. 9 8
      client/rest-high-level/src/main/java/org/elasticsearch/client/TasksRequestConverters.java
  4. 151 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/CancelTasksRequest.java
  5. 91 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/CancelTasksResponse.java
  6. 225 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/ElasticsearchException.java
  7. 2 2
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/GetTaskRequest.java
  8. 5 6
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/GetTaskResponse.java
  9. 138 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/ListTasksResponse.java
  10. 160 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/NodeData.java
  11. 101 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskGroup.java
  12. 91 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskId.java
  13. 194 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskInfo.java
  14. 107 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskOperationFailure.java
  15. 10 11
      client/rest-high-level/src/test/java/org/elasticsearch/client/TasksIT.java
  16. 9 6
      client/rest-high-level/src/test/java/org/elasticsearch/client/TasksRequestConvertersTests.java
  17. 16 14
      client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/TasksClientDocumentationIT.java
  18. 218 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/tasks/CancelTasksResponseTests.java
  19. 83 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/tasks/ElasticsearchExceptionTests.java

+ 26 - 3
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -53,6 +53,7 @@ import org.elasticsearch.client.core.MultiTermVectorsRequest;
 import org.elasticsearch.client.core.TermVectorsRequest;
 import org.elasticsearch.client.indices.AnalyzeRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
+import org.elasticsearch.client.tasks.TaskId;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.Priority;
@@ -80,13 +81,13 @@ import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
 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;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -1029,19 +1030,41 @@ final class RequestConverters {
         }
 
         Params withNodes(String[] nodes) {
-            if (nodes != null && nodes.length > 0) {
+           return withNodes(Arrays.asList(nodes));
+        }
+
+        Params withNodes(List<String> nodes) {
+            if (nodes != null && nodes.size() > 0) {
                 return putParam("nodes", String.join(",", nodes));
             }
             return this;
         }
 
         Params withActions(String[] actions) {
-            if (actions != null && actions.length > 0) {
+            return withActions(Arrays.asList(actions));
+        }
+
+        Params withActions(List<String> actions) {
+            if (actions != null && actions.size() > 0) {
                 return putParam("actions", String.join(",", actions));
             }
             return this;
         }
 
+        Params withTaskId(org.elasticsearch.tasks.TaskId taskId) {
+            if (taskId != null && taskId.isSet()) {
+                return putParam("task_id", taskId.toString());
+            }
+            return this;
+        }
+
+        Params withParentTaskId(org.elasticsearch.tasks.TaskId parentTaskId) {
+            if (parentTaskId != null && parentTaskId.isSet()) {
+                return putParam("parent_task_id", parentTaskId.toString());
+            }
+            return this;
+        }
+
         Params withTaskId(TaskId taskId) {
             if (taskId != null && taskId.isSet()) {
                 return putParam("task_id", taskId.toString());

+ 2 - 2
client/rest-high-level/src/main/java/org/elasticsearch/client/TasksClient.java

@@ -20,10 +20,10 @@
 package org.elasticsearch.client;
 
 import org.elasticsearch.action.ActionListener;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse;
+import org.elasticsearch.client.tasks.CancelTasksRequest;
+import org.elasticsearch.client.tasks.CancelTasksResponse;
 import org.elasticsearch.client.tasks.GetTaskRequest;
 import org.elasticsearch.client.tasks.GetTaskResponse;
 

+ 9 - 8
client/rest-high-level/src/main/java/org/elasticsearch/client/TasksRequestConverters.java

@@ -21,23 +21,24 @@ package org.elasticsearch.client;
 
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
 import org.elasticsearch.client.RequestConverters.EndpointBuilder;
+import org.elasticsearch.client.tasks.CancelTasksRequest;
 import org.elasticsearch.client.tasks.GetTaskRequest;
 
 final class TasksRequestConverters {
 
     private TasksRequestConverters() {}
 
-    static Request cancelTasks(CancelTasksRequest cancelTasksRequest) {
+    static Request cancelTasks(CancelTasksRequest req) {
         Request request = new Request(HttpPost.METHOD_NAME, "/_tasks/_cancel");
         RequestConverters.Params params = new RequestConverters.Params();
-        params.withTimeout(cancelTasksRequest.getTimeout())
-            .withTaskId(cancelTasksRequest.getTaskId())
-            .withNodes(cancelTasksRequest.getNodes())
-            .withParentTaskId(cancelTasksRequest.getParentTaskId())
-            .withActions(cancelTasksRequest.getActions());
+        req.getTimeout().ifPresent(params::withTimeout);
+        req.getTaskId().ifPresent(params::withTaskId);
+        req.getParentTaskId().ifPresent(params::withParentTaskId);
+        params
+            .withNodes(req.getNodes())
+            .withActions(req.getActions());
         request.addParameters(params.asMap());
         return request;
     }
@@ -70,5 +71,5 @@ final class TasksRequestConverters {
         request.addParameters(params.asMap());
         return request;
     }
-    
+
 }

+ 151 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/CancelTasksRequest.java

@@ -0,0 +1,151 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.common.unit.TimeValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class CancelTasksRequest implements Validatable {
+
+    private final List<String> nodes = new ArrayList<>();
+    private final List<String> actions = new ArrayList<>();
+    private Optional<TimeValue> timeout = Optional.empty();
+    private Optional<TaskId> parentTaskId = Optional.empty();
+    private Optional<TaskId> taskId = Optional.empty();
+
+    CancelTasksRequest(){}
+
+    void setNodes(List<String> nodes) {
+        this.nodes.addAll(nodes);
+    }
+
+    public List<String> getNodes() {
+        return nodes;
+    }
+
+    void setTimeout(TimeValue timeout) {
+        this.timeout = Optional.of(timeout);
+    }
+
+    public Optional<TimeValue> getTimeout() {
+        return timeout;
+    }
+
+    void setActions(List<String> actions) {
+        this.actions.addAll(actions);
+    }
+
+    public List<String> getActions() {
+        return actions;
+    }
+
+    void setParentTaskId(TaskId parentTaskId) {
+        this.parentTaskId = Optional.of(parentTaskId);
+    }
+
+    public Optional<TaskId> getParentTaskId() {
+        return parentTaskId;
+    }
+
+    void setTaskId(TaskId taskId) {
+        this.taskId = Optional.of(taskId);
+    }
+
+    public Optional<TaskId> getTaskId() {
+        return taskId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof CancelTasksRequest)) return false;
+        CancelTasksRequest that = (CancelTasksRequest) o;
+        return Objects.equals(getNodes(), that.getNodes()) &&
+            Objects.equals(getActions(), that.getActions()) &&
+            Objects.equals(getTimeout(), that.getTimeout()) &&
+            Objects.equals(getParentTaskId(), that.getParentTaskId()) &&
+            Objects.equals(getTaskId(), that.getTaskId()) ;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getNodes(), getActions(), getTimeout(), getParentTaskId(), getTaskId());
+    }
+
+    @Override
+    public String toString() {
+        return "CancelTasksRequest{" +
+            "nodes=" + nodes +
+            ", actions=" + actions +
+            ", timeout=" + timeout +
+            ", parentTaskId=" + parentTaskId +
+            ", taskId=" + taskId +
+            '}';
+    }
+
+    public static class Builder {
+        private Optional<TimeValue> timeout = Optional.empty();
+        private Optional<TaskId> taskId = Optional.empty();
+        private Optional<TaskId> parentTaskId = Optional.empty();
+        private List<String> actionsFilter = new ArrayList<>();
+        private List<String> nodesFilter = new ArrayList<>();
+
+        public Builder withTimeout(TimeValue timeout){
+            this.timeout = Optional.of(timeout);
+            return this;
+        }
+
+        public Builder withTaskId(TaskId taskId){
+            this.taskId = Optional.of(taskId);
+            return this;
+        }
+
+        public Builder withParentTaskId(TaskId taskId){
+            this.parentTaskId = Optional.of(taskId);
+            return this;
+        }
+
+        public Builder withActionsFiltered(List<String> actions){
+            this.actionsFilter.clear();
+            this.actionsFilter.addAll(actions);
+            return this;
+        }
+
+        public Builder withNodesFiltered(List<String> nodes){
+            this.nodesFilter.clear();
+            this.nodesFilter.addAll(nodes);
+            return this;
+        }
+
+        public CancelTasksRequest build() {
+            CancelTasksRequest request = new CancelTasksRequest();
+            timeout.ifPresent(request::setTimeout);
+            taskId.ifPresent(request::setTaskId);
+            parentTaskId.ifPresent(request::setParentTaskId);
+            request.setNodes(nodesFilter);
+            request.setActions(actionsFilter);
+            return request;
+        }
+    }
+}

+ 91 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/CancelTasksResponse.java

@@ -0,0 +1,91 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * cancel tasks response that contains
+ * - task failures
+ * - node failures
+ * - tasks
+ */
+public class CancelTasksResponse extends ListTasksResponse {
+
+    CancelTasksResponse(List<NodeData> nodesInfoData,
+                        List<TaskOperationFailure> taskFailures,
+                        List<ElasticsearchException> nodeFailures) {
+        super(nodesInfoData, taskFailures, nodeFailures);
+    }
+
+    public static CancelTasksResponse fromXContent(final XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    private static ConstructingObjectParser<CancelTasksResponse, Void> PARSER;
+
+    static {
+        ConstructingObjectParser<CancelTasksResponse, Void> parser = new ConstructingObjectParser<>("cancel_tasks_response", true,
+            constructingObjects -> {
+                int i = 0;
+                @SuppressWarnings("unchecked")
+                List<TaskOperationFailure> tasksFailures = (List<TaskOperationFailure>) constructingObjects[i++];
+                @SuppressWarnings("unchecked")
+                List<ElasticsearchException> nodeFailures = (List<ElasticsearchException>) constructingObjects[i++];
+                @SuppressWarnings("unchecked")
+                List<NodeData> nodesInfoData = (List<NodeData>) constructingObjects[i];
+                return new CancelTasksResponse(nodesInfoData, tasksFailures, nodeFailures);
+            });
+
+        parser.declareObjectArray(optionalConstructorArg(), (p, c) ->
+            TaskOperationFailure.fromXContent(p), new ParseField("task_failures"));
+        parser.declareObjectArray(optionalConstructorArg(), (p, c) ->
+            ElasticsearchException.fromXContent(p), new ParseField("node_failures"));
+        parser.declareNamedObjects(optionalConstructorArg(), NodeData.PARSER, new ParseField("nodes"));
+        PARSER = parser;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return super.equals(o);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "CancelTasksResponse{" +
+            "taskFailures=" + taskFailures +
+            ", nodeFailures=" + nodeFailures +
+            ", nodesInfoData=" + nodesInfoData +
+            ", tasks=" + tasks +
+            ", taskGroups=" + taskGroups +
+            '}';
+    }
+}

+ 225 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/ElasticsearchException.java

@@ -0,0 +1,225 @@
+/*
+ * 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.client.tasks;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.XContentParser;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
+
+/**
+ * client side counterpart of server side
+ * {@link org.elasticsearch.ElasticsearchException}
+ * It wraps the same content but it is not throwable.
+ */
+public class ElasticsearchException {
+
+    private static final String TYPE = "type";
+    private static final String REASON = "reason";
+    private static final String CAUSED_BY = "caused_by";
+    private static final ParseField SUPPRESSED = new ParseField("suppressed");
+    private static final String STACK_TRACE = "stack_trace";
+    private static final String HEADER = "header";
+    private static final String ROOT_CAUSE = "root_cause";
+
+    private String msg;
+    private ElasticsearchException cause;
+    private final Map<String, List<String>> headers = new HashMap<>();
+    private final List<ElasticsearchException> suppressed = new ArrayList<>();
+
+    ElasticsearchException(String msg) {
+        this.msg = msg;
+        this.cause = null;
+    }
+
+    ElasticsearchException(String msg, ElasticsearchException cause) {
+        this.msg = msg;
+        this.cause = cause;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public ElasticsearchException getCause() {
+        return cause;
+    }
+
+    public List<ElasticsearchException> getSuppressed() {
+        return suppressed;
+    }
+
+    void addSuppressed(List<ElasticsearchException> suppressed){
+        this.suppressed.addAll(suppressed);
+    }
+
+    /**
+     * Generate a {@link ElasticsearchException} from a {@link XContentParser}. This does not
+     * return the original exception type (ie NodeClosedException for example) but just wraps
+     * the type, the reason and the cause of the exception. It also recursively parses the
+     * tree structure of the cause, returning it as a tree structure of {@link ElasticsearchException}
+     * instances.
+     */
+    static ElasticsearchException fromXContent(XContentParser parser) throws IOException {
+        XContentParser.Token token = parser.nextToken();
+        ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
+        return innerFromXContent(parser, false);
+    }
+
+    private static ElasticsearchException innerFromXContent(XContentParser parser, boolean parseRootCauses) throws IOException {
+        XContentParser.Token token = parser.currentToken();
+        ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
+
+        String type = null, reason = null, stack = null;
+        ElasticsearchException cause = null;
+        Map<String, List<String>> headers = new HashMap<>();
+        List<ElasticsearchException> rootCauses = new ArrayList<>();
+        List<ElasticsearchException> suppressed = new ArrayList<>();
+
+        for (; token == XContentParser.Token.FIELD_NAME; token = parser.nextToken()) {
+            String currentFieldName = parser.currentName();
+            token = parser.nextToken();
+
+            if (token.isValue()) {
+                if (TYPE.equals(currentFieldName)) {
+                    type = parser.text();
+                } else if (REASON.equals(currentFieldName)) {
+                    reason = parser.text();
+                } else if (STACK_TRACE.equals(currentFieldName)) {
+                    stack = parser.text();
+                }
+            } else if (token == XContentParser.Token.START_OBJECT) {
+                if (CAUSED_BY.equals(currentFieldName)) {
+                    cause = fromXContent(parser);
+                } else if (HEADER.equals(currentFieldName)) {
+                    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+                        if (token == XContentParser.Token.FIELD_NAME) {
+                            currentFieldName = parser.currentName();
+                        } else {
+                            List<String> values = headers.getOrDefault(currentFieldName, new ArrayList<>());
+                            if (token == XContentParser.Token.VALUE_STRING) {
+                                values.add(parser.text());
+                            } else if (token == XContentParser.Token.START_ARRAY) {
+                                while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
+                                    if (token == XContentParser.Token.VALUE_STRING) {
+                                        values.add(parser.text());
+                                    } else {
+                                        parser.skipChildren();
+                                    }
+                                }
+                            } else if (token == XContentParser.Token.START_OBJECT) {
+                                parser.skipChildren();
+                            }
+                            headers.put(currentFieldName, values);
+                        }
+                    }
+                } else {
+                    // Any additional metadata object added by the metadataToXContent method is ignored
+                    // and skipped, so that the parser does not fail on unknown fields. The parser only
+                    // support metadata key-pairs and metadata arrays of values.
+                    parser.skipChildren();
+                }
+            } else if (token == XContentParser.Token.START_ARRAY) {
+                if (parseRootCauses && ROOT_CAUSE.equals(currentFieldName)) {
+                    while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
+                        rootCauses.add(fromXContent(parser));
+                    }
+                } else if (SUPPRESSED.match(currentFieldName, parser.getDeprecationHandler())) {
+                    while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
+                        suppressed.add(fromXContent(parser));
+                    }
+                } else {
+                    // Parse the array and add each item to the corresponding list of metadata.
+                    // Arrays of objects are not supported yet and just ignored and skipped.
+                    List<String> values = new ArrayList<>();
+                    while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
+                        if (token == XContentParser.Token.VALUE_STRING) {
+                            values.add(parser.text());
+                        } else {
+                            parser.skipChildren();
+                        }
+                    }
+                }
+            }
+        }
+
+        ElasticsearchException e = new ElasticsearchException(buildMessage(type, reason, stack), cause);
+        for (Map.Entry<String, List<String>> header : headers.entrySet()) {
+            e.addHeader(header.getKey(), header.getValue());
+        }
+
+        // Adds root causes as suppressed exception. This way they are not lost
+        // after parsing and can be retrieved using getSuppressed() method.
+        e.suppressed.addAll(rootCauses);
+        e.suppressed.addAll(suppressed);
+
+        return e;
+    }
+
+    void addHeader(String key, List<String> value) {
+        headers.put(key,value);
+
+    }
+
+    public Map<String, List<String>> getHeaders() {
+        return headers;
+    }
+
+    static String buildMessage(String type, String reason, String stack) {
+        StringBuilder message = new StringBuilder("Elasticsearch exception [");
+        message.append(TYPE).append('=').append(type).append(", ");
+        message.append(REASON).append('=').append(reason);
+        if (stack != null) {
+            message.append(", ").append(STACK_TRACE).append('=').append(stack);
+        }
+        message.append(']');
+        return message.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ElasticsearchException)) return false;
+        ElasticsearchException that = (ElasticsearchException) o;
+        return Objects.equals(getMsg(), that.getMsg()) &&
+            Objects.equals(getCause(), that.getCause()) &&
+            Objects.equals(getHeaders(), that.getHeaders()) &&
+            Objects.equals(getSuppressed(), that.getSuppressed());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getMsg(), getCause(), getHeaders(), getSuppressed());
+    }
+
+    @Override
+    public String toString() {
+        return "ElasticsearchException{" +
+            "msg='" + msg + '\'' +
+            ", cause=" + cause +
+            ", headers=" + headers +
+            ", suppressed=" + suppressed +
+            '}';
+    }
+}

+ 2 - 2
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/GetTaskRequest.java

@@ -101,8 +101,8 @@ public class GetTaskRequest implements Validatable {
         }
         GetTaskRequest other = (GetTaskRequest) obj;
         return Objects.equals(nodeId, other.nodeId) &&
-                taskId == other.taskId && 
-                waitForCompletion == other.waitForCompletion && 
+                taskId == other.taskId &&
+                waitForCompletion == other.waitForCompletion &&
                 Objects.equals(timeout, other.timeout);
     }
 }

+ 5 - 6
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/GetTaskResponse.java

@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.elasticsearch.client.tasks;
 
 import org.elasticsearch.common.ParseField;
@@ -31,16 +30,16 @@ public class GetTaskResponse {
     private final TaskInfo taskInfo;
     public static final ParseField COMPLETED = new ParseField("completed");
     public static final ParseField TASK = new ParseField("task");
-    
+
     public GetTaskResponse(boolean completed, TaskInfo taskInfo) {
         this.completed = completed;
         this.taskInfo = taskInfo;
     }
-    
+
     public boolean isCompleted() {
         return completed;
     }
-    
+
     public TaskInfo getTaskInfo() {
         return taskInfo;
     }
@@ -50,9 +49,9 @@ public class GetTaskResponse {
     static {
         PARSER.declareBoolean(constructorArg(), COMPLETED);
         PARSER.declareObject(constructorArg(), (p, c) -> TaskInfo.fromXContent(p), TASK);
-    }    
+    }
 
     public static GetTaskResponse fromXContent(XContentParser parser) {
         return PARSER.apply(parser, null);
-    }    
+    }
 }

+ 138 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/ListTasksResponse.java

@@ -0,0 +1,138 @@
+/*
+ * 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.client.tasks;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
+
+public class ListTasksResponse {
+
+    protected final List<TaskOperationFailure> taskFailures = new ArrayList<>();
+    protected final List<ElasticsearchException> nodeFailures = new ArrayList<>();
+    protected final List<NodeData> nodesInfoData = new ArrayList<>();
+    protected final List<TaskInfo> tasks = new ArrayList<>();
+    protected final List<TaskGroup> taskGroups = new ArrayList<>();
+
+    ListTasksResponse(List<NodeData> nodesInfoData,
+                        List<TaskOperationFailure> taskFailures,
+                        List<ElasticsearchException> nodeFailures) {
+        if (taskFailures != null) {
+            this.taskFailures.addAll(taskFailures);
+        }
+        if (nodeFailures != null) {
+            this.nodeFailures.addAll(nodeFailures);
+        }
+        if (nodesInfoData != null) {
+            this.nodesInfoData.addAll(nodesInfoData);
+        }
+        this.tasks.addAll(this
+            .nodesInfoData
+            .stream()
+            .flatMap(nodeData -> nodeData.getTasks().stream())
+            .collect(toList())
+        );
+        this.taskGroups.addAll(buildTaskGroups());
+    }
+
+    private List<TaskGroup> buildTaskGroups() {
+        Map<TaskId, TaskGroup.Builder> taskGroups = new HashMap<>();
+        List<TaskGroup.Builder> topLevelTasks = new ArrayList<>();
+        // First populate all tasks
+        for (TaskInfo taskInfo : this.tasks) {
+            taskGroups.put(taskInfo.getTaskId(), TaskGroup.builder(taskInfo));
+        }
+
+        // Now go through all task group builders and add children to their parents
+        for (TaskGroup.Builder taskGroup : taskGroups.values()) {
+            TaskId parentTaskId = taskGroup.getTaskInfo().getParentTaskId();
+            if (parentTaskId != null) {
+                TaskGroup.Builder parentTask = taskGroups.get(parentTaskId);
+                if (parentTask != null) {
+                    // we found parent in the list of tasks - add it to the parent list
+                    parentTask.addGroup(taskGroup);
+                } else {
+                    // we got zombie or the parent was filtered out - add it to the top task list
+                    topLevelTasks.add(taskGroup);
+                }
+            } else {
+                // top level task - add it to the top task list
+                topLevelTasks.add(taskGroup);
+            }
+        }
+        return topLevelTasks.stream().map(TaskGroup.Builder::build).collect(Collectors.toUnmodifiableList());
+    }
+
+    public List<TaskInfo> getTasks() {
+        return tasks;
+    }
+
+    public Map<String, List<TaskInfo>> getPerNodeTasks() {
+        return getTasks()
+            .stream()
+            .collect(groupingBy(TaskInfo::getNodeId));
+    }
+
+    public List<TaskOperationFailure> getTaskFailures() {
+        return taskFailures;
+    }
+
+    public List<ElasticsearchException> getNodeFailures() {
+        return nodeFailures;
+    }
+
+    public List<TaskGroup> getTaskGroups() {
+        return taskGroups;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ListTasksResponse)) return false;
+        ListTasksResponse response = (ListTasksResponse) o;
+        return nodesInfoData.equals(response.nodesInfoData) &&
+            Objects.equals
+                (getTaskFailures(), response.getTaskFailures()) &&
+            Objects.equals(getNodeFailures(), response.getNodeFailures()) &&
+            Objects.equals(getTasks(), response.getTasks()) &&
+            Objects.equals(getTaskGroups(), response.getTaskGroups());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(nodesInfoData, getTaskFailures(), getNodeFailures(), getTasks(), getTaskGroups());
+    }
+
+    @Override
+    public String toString() {
+        return "CancelTasksResponse{" +
+            "nodesInfoData=" + nodesInfoData +
+            ", taskFailures=" + taskFailures +
+            ", nodeFailures=" + nodeFailures +
+            ", tasks=" + tasks +
+            ", taskGroups=" + taskGroups +
+            '}';
+    }
+}

+ 160 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/NodeData.java

@@ -0,0 +1,160 @@
+/*
+ * 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.client.tasks;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+class NodeData {
+
+    private String nodeId;
+    private String name;
+    private String transportAddress;
+    private String host;
+    private String ip;
+    private final List<String> roles = new ArrayList<>();
+    private final Map<String,String> attributes = new HashMap<>();
+    private final List<TaskInfo> tasks = new ArrayList<>();
+
+    NodeData(String nodeId) {
+        this.nodeId = nodeId;
+    }
+
+    void setName(String name) {
+        this.name = name;
+    }
+
+    public void setAttributes(Map<String, String> attributes) {
+        if(attributes!=null){
+            this.attributes.putAll(attributes);
+        }
+    }
+
+    void setTransportAddress(String transportAddress) {
+        this.transportAddress = transportAddress;
+    }
+
+    void setHost(String host) {
+        this.host = host;
+    }
+
+    void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    void setRoles(List<String> roles) {
+        if(roles!=null){
+            this.roles.addAll(roles);
+        }
+    }
+
+    public String getNodeId() {
+        return nodeId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getTransportAddress() {
+        return transportAddress;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public List<String> getRoles() {
+        return roles;
+    }
+
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    public List<TaskInfo> getTasks() {
+        return tasks;
+    }
+
+    void setTasks(List<TaskInfo> tasks) {
+        if(tasks!=null){
+            this.tasks.addAll(tasks);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "NodeData{" +
+            "nodeId='" + nodeId + '\'' +
+            ", name='" + name + '\'' +
+            ", transportAddress='" + transportAddress + '\'' +
+            ", host='" + host + '\'' +
+            ", ip='" + ip + '\'' +
+            ", roles=" + roles +
+            ", attributes=" + attributes +
+            '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof NodeData)) return false;
+        NodeData nodeData = (NodeData) o;
+        return Objects.equals(getNodeId(), nodeData.getNodeId()) &&
+            Objects.equals(getName(), nodeData.getName()) &&
+            Objects.equals(getTransportAddress(), nodeData.getTransportAddress()) &&
+            Objects.equals(getHost(), nodeData.getHost()) &&
+            Objects.equals(getIp(), nodeData.getIp()) &&
+            Objects.equals(getRoles(), nodeData.getRoles()) &&
+            Objects.equals(getAttributes(), nodeData.getAttributes()) &&
+            Objects.equals(getTasks(), nodeData.getTasks());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getNodeId(), getName(), getTransportAddress(), getHost(), getIp(), getRoles(), getAttributes(), getTasks());
+    }
+
+    public static final ObjectParser.NamedObjectParser<NodeData, Void> PARSER;
+
+    static {
+        ObjectParser<NodeData, Void> parser = new ObjectParser<>("nodes");
+        parser.declareString(NodeData::setName, new ParseField("name"));
+        parser.declareString(NodeData::setTransportAddress, new ParseField("transport_address"));
+        parser.declareString(NodeData::setHost, new ParseField("host"));
+        parser.declareString(NodeData::setIp, new ParseField("ip"));
+        parser.declareStringArray(NodeData::setRoles, new ParseField("roles"));
+        parser.declareField(NodeData::setAttributes,
+           (p, c) -> p.mapStrings(),
+           new ParseField("attributes"),
+           ObjectParser.ValueType.OBJECT);
+        parser.declareNamedObjects(NodeData::setTasks, TaskInfo.PARSER, new ParseField("tasks"));
+        PARSER = (XContentParser p, Void v, String nodeId) -> parser.parse(p, new NodeData(nodeId), null);
+    }
+}

+ 101 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskGroup.java

@@ -0,0 +1,101 @@
+/*
+ * 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.client.tasks;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Client side counterpart of server side version.
+ *
+ * {@link org.elasticsearch.action.admin.cluster.node.tasks.list.TaskGroup}
+ */
+public class TaskGroup {
+
+    private final TaskInfo task;
+
+    @Override
+    public String toString() {
+        return "TaskGroup{" +
+            "task=" + task +
+            ", childTasks=" + childTasks +
+            '}';
+    }
+
+    private final List<TaskGroup> childTasks = new ArrayList<>();
+
+    public TaskGroup(TaskInfo task, List<TaskGroup> childTasks) {
+        this.task = task;
+        this.childTasks.addAll(childTasks);
+    }
+
+    public static TaskGroup.Builder builder(TaskInfo taskInfo) {
+        return new TaskGroup.Builder(taskInfo);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TaskGroup)) return false;
+        TaskGroup taskGroup = (TaskGroup) o;
+        return Objects.equals(task, taskGroup.task) &&
+            Objects.equals(getChildTasks(), taskGroup.getChildTasks());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(task, getChildTasks());
+    }
+
+    public static class Builder {
+        private TaskInfo taskInfo;
+        private List<TaskGroup.Builder> childTasks;
+
+        private Builder(TaskInfo taskInfo) {
+            this.taskInfo = taskInfo;
+            childTasks = new ArrayList<>();
+        }
+
+        public void addGroup(TaskGroup.Builder builder) {
+            childTasks.add(builder);
+        }
+
+        public TaskInfo getTaskInfo() {
+            return taskInfo;
+        }
+
+        public TaskGroup build() {
+            return new TaskGroup(
+                taskInfo,
+                childTasks.stream().map(TaskGroup.Builder::build).collect(Collectors.toList())
+            );
+        }
+    }
+
+    public TaskInfo getTaskInfo() {
+        return task;
+    }
+
+    public List<TaskGroup> getChildTasks() {
+        return childTasks;
+    }
+}
+

+ 91 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskId.java

@@ -0,0 +1,91 @@
+/*
+ * 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.client.tasks;
+
+import java.util.Objects;
+
+/**
+ * client side version of a {@link org.elasticsearch.tasks.TaskId}
+ */
+public class TaskId {
+
+    protected final String nodeId;
+    protected final long id;
+
+    public TaskId(String nodeId, long id) {
+        this.nodeId = nodeId;
+        this.id = id;
+    }
+
+    /**
+     * accepts a raw format task id
+     * @param taskId expected to be nodeid:taskId
+     */
+    public TaskId(String taskId) {
+        if (taskId == null) {
+            throw new IllegalArgumentException("null task id");
+        }
+        String[] s = taskId.split(":");
+        if (s.length != 2) {
+            throw new IllegalArgumentException("malformed task id " + taskId);
+        }
+        this.nodeId = s[0];
+        try {
+            this.id = Long.parseLong(s[1]);
+        } catch (NumberFormatException ex) {
+            throw new IllegalArgumentException("malformed task id " + taskId, ex);
+        }
+    }
+
+    public String getNodeId() {
+        return nodeId;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public boolean isSet() {
+        return id != -1L;
+    }
+
+    @Override
+    public String toString() {
+        if (isSet()) {
+            return nodeId + ":" + id;
+        } else {
+            return "unset";
+        }
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TaskId)) return false;
+        TaskId taskId = (TaskId) o;
+        return getId() == taskId.getId() &&
+            Objects.equals(getNodeId(), taskId.getNodeId());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getNodeId(), getId());
+    }
+}

+ 194 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskInfo.java

@@ -0,0 +1,194 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * client side counterpart of server side
+ * <p>
+ * {@link org.elasticsearch.tasks.TaskInfo}
+ */
+public class TaskInfo {
+
+    private TaskId taskId;
+    private String type;
+    private String action;
+    private String description;
+    private long startTime;
+    private long runningTimeNanos;
+    private boolean cancellable;
+    private TaskId parentTaskId;
+    private final Map<String, Object> status = new HashMap<>();
+    private final Map<String, String> headers = new HashMap<>();
+
+    public TaskInfo(TaskId taskId) {
+        this.taskId = taskId;
+    }
+
+    public TaskId getTaskId() {
+        return taskId;
+    }
+
+    public String getNodeId() {
+        return taskId.nodeId;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    void setType(String type) {
+        this.type = type;
+    }
+
+    public String getAction() {
+        return action;
+    }
+
+    void setAction(String action) {
+        this.action = action;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    void setDescription(String description) {
+        this.description = description;
+    }
+
+    public long getStartTime() {
+        return startTime;
+    }
+
+    void setStartTime(long startTime) {
+        this.startTime = startTime;
+    }
+
+    public long getRunningTimeNanos() {
+        return runningTimeNanos;
+    }
+
+    void setRunningTimeNanos(long runningTimeNanos) {
+        this.runningTimeNanos = runningTimeNanos;
+    }
+
+    public boolean isCancellable() {
+        return cancellable;
+    }
+
+    void setCancellable(boolean cancellable) {
+        this.cancellable = cancellable;
+    }
+
+    public TaskId getParentTaskId() {
+        return parentTaskId;
+    }
+
+    void setParentTaskId(String parentTaskId) {
+        this.parentTaskId = new TaskId(parentTaskId);
+    }
+
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
+
+    void setHeaders(Map<String, String> headers) {
+        this.headers.putAll(headers);
+    }
+
+    void setStatus(Map<String, Object> status) {
+        this.status.putAll(status);
+    }
+
+    public Map<String, Object> getStatus() {
+        return status;
+    }
+
+    private void noOpParse(Object s) {}
+
+    public static final ObjectParser.NamedObjectParser<TaskInfo, Void> PARSER;
+
+    static {
+        ObjectParser<TaskInfo, Void> parser = new ObjectParser<>("tasks", true, null);
+        // already provided in constructor: triggering a no-op
+        parser.declareString(TaskInfo::noOpParse, new ParseField("node"));
+        // already provided in constructor: triggering a no-op
+        parser.declareLong(TaskInfo::noOpParse, new ParseField("id"));
+        parser.declareString(TaskInfo::setType, new ParseField("type"));
+        parser.declareString(TaskInfo::setAction, new ParseField("action"));
+        parser.declareObject(TaskInfo::setStatus, (p, c) -> p.map(), new ParseField("status"));
+        parser.declareString(TaskInfo::setDescription, new ParseField("description"));
+        parser.declareLong(TaskInfo::setStartTime, new ParseField("start_time_in_millis"));
+        parser.declareLong(TaskInfo::setRunningTimeNanos, new ParseField("running_time_in_nanos"));
+        parser.declareBoolean(TaskInfo::setCancellable, new ParseField("cancellable"));
+        parser.declareString(TaskInfo::setParentTaskId, new ParseField("parent_task_id"));
+        parser.declareObject(TaskInfo::setHeaders, (p, c) -> p.mapStrings(), new ParseField("headers"));
+        PARSER = (XContentParser p, Void v, String name) -> parser.parse(p, new TaskInfo(new TaskId(name)), null);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TaskInfo)) return false;
+        TaskInfo taskInfo = (TaskInfo) o;
+        return getStartTime() == taskInfo.getStartTime() &&
+            getRunningTimeNanos() == taskInfo.getRunningTimeNanos() &&
+            isCancellable() == taskInfo.isCancellable() &&
+            Objects.equals(getTaskId(), taskInfo.getTaskId()) &&
+            Objects.equals(getType(), taskInfo.getType()) &&
+            Objects.equals(getAction(), taskInfo.getAction()) &&
+            Objects.equals(getDescription(), taskInfo.getDescription()) &&
+            Objects.equals(getParentTaskId(), taskInfo.getParentTaskId()) &&
+            Objects.equals(status, taskInfo.status) &&
+            Objects.equals(getHeaders(), taskInfo.getHeaders());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+            getTaskId(), getType(), getAction(), getDescription(), getStartTime(),
+            getRunningTimeNanos(), isCancellable(), getParentTaskId(), status, getHeaders()
+        );
+    }
+
+
+    @Override
+    public String toString() {
+        return "TaskInfo{" +
+            "taskId=" + taskId +
+            ", type='" + type + '\'' +
+            ", action='" + action + '\'' +
+            ", description='" + description + '\'' +
+            ", startTime=" + startTime +
+            ", runningTimeNanos=" + runningTimeNanos +
+            ", cancellable=" + cancellable +
+            ", parentTaskId=" + parentTaskId +
+            ", status=" + status +
+            ", headers=" + headers +
+            '}';
+    }
+}

+ 107 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/tasks/TaskOperationFailure.java

@@ -0,0 +1,107 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+
+/**
+ * client side counterpart of server side
+ * {@link org.elasticsearch.action.TaskOperationFailure}
+ */
+public class TaskOperationFailure {
+
+    private final String nodeId;
+    private final long taskId;
+    private final ElasticsearchException reason;
+    private final String status;
+
+    public TaskOperationFailure(String nodeId, long taskId,String status, ElasticsearchException reason) {
+        this.nodeId = nodeId;
+        this.taskId = taskId;
+        this.status = status;
+        this.reason = reason;
+    }
+
+    public String getNodeId() {
+        return nodeId;
+    }
+
+    public long getTaskId() {
+        return taskId;
+    }
+
+    public ElasticsearchException getReason() {
+        return reason;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TaskOperationFailure)) return false;
+        TaskOperationFailure that = (TaskOperationFailure) o;
+        return getTaskId() == that.getTaskId() &&
+            Objects.equals(getNodeId(), that.getNodeId()) &&
+            Objects.equals(getReason(), that.getReason()) &&
+            Objects.equals(getStatus(), that.getStatus());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getNodeId(), getTaskId(), getReason(), getStatus());
+    }
+    @Override
+    public String toString() {
+        return "TaskOperationFailure{" +
+            "nodeId='" + nodeId + '\'' +
+            ", taskId=" + taskId +
+            ", reason=" + reason +
+            ", status='" + status + '\'' +
+            '}';
+    }
+    public static TaskOperationFailure fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    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++];
+            String status = (String) constructorObjects[i++];
+            ElasticsearchException reason = (ElasticsearchException) constructorObjects[i];
+            return new TaskOperationFailure(nodeId, taskId, status, reason);
+        });
+
+    static {
+        PARSER.declareString(constructorArg(), new ParseField("node_id"));
+        PARSER.declareLong(constructorArg(), new ParseField("task_id"));
+        PARSER.declareString(constructorArg(), new ParseField("status"));
+        PARSER.declareObject(constructorArg(), (parser, c) -> ElasticsearchException.fromXContent(parser), new ParseField("reason"));
+    }
+}

+ 10 - 11
client/rest-high-level/src/test/java/org/elasticsearch/client/TasksIT.java

@@ -19,21 +19,20 @@
 
 package org.elasticsearch.client;
 
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse;
 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.bulk.BulkRequest;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
+import org.elasticsearch.client.tasks.CancelTasksRequest;
+import org.elasticsearch.client.tasks.CancelTasksResponse;
 import org.elasticsearch.client.tasks.GetTaskRequest;
 import org.elasticsearch.client.tasks.GetTaskResponse;
+import org.elasticsearch.client.tasks.TaskId;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.rest.RestStatus;
-import org.elasticsearch.tasks.TaskId;
-import org.elasticsearch.tasks.TaskInfo;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -58,12 +57,12 @@ public class TasksIT extends ESRestHighLevelClientTestCase {
         assertThat(response.getTasks().size(), greaterThanOrEqualTo(2));
         boolean listTasksFound = false;
         for (TaskGroup taskGroup : response.getTaskGroups()) {
-            TaskInfo parent = taskGroup.getTaskInfo();
+            org.elasticsearch.tasks.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();
+                org.elasticsearch.tasks.TaskInfo child = childGroup.getTaskInfo();
                 assertThat(child.getAction(), equalTo("cluster:monitor/tasks/lists[n]"));
                 assertThat(child.getParentTaskId(), equalTo(parent.getTaskId()));
                 listTasksFound = true;
@@ -117,7 +116,7 @@ public class TasksIT extends ESRestHighLevelClientTestCase {
         if (gtr.getWaitForCompletion()) {
             assertTrue(taskResponse.isCompleted());
         }
-        TaskInfo info = taskResponse.getTaskInfo();
+        org.elasticsearch.tasks.TaskInfo info = taskResponse.getTaskInfo();
         assertTrue(info.isCancellable());
         assertEquals("reindex from [source1] to [dest]", info.getDescription());
         assertEquals("indices:data/write/reindex", info.getAction());
@@ -142,12 +141,12 @@ public class TasksIT extends ESRestHighLevelClientTestCase {
         );
         // in this case, probably no task will actually be cancelled.
         // this is ok, that case is covered in TasksIT.testTasksCancellation
-        TaskInfo firstTask = listResponse.getTasks().get(0);
+        org.elasticsearch.tasks.TaskInfo firstTask = listResponse.getTasks().get(0);
         String node = listResponse.getPerNodeTasks().keySet().iterator().next();
 
-        CancelTasksRequest cancelTasksRequest = new CancelTasksRequest();
-        cancelTasksRequest.setTaskId(new TaskId(node, firstTask.getId()));
-        cancelTasksRequest.setReason("testreason");
+        CancelTasksRequest cancelTasksRequest =  new CancelTasksRequest.Builder().withTaskId(
+            new TaskId(node, firstTask.getId())
+        ).build();
         CancelTasksResponse response = execute(cancelTasksRequest,
             highLevelClient().tasks()::cancel,
             highLevelClient().tasks()::cancelAsync);

+ 9 - 6
client/rest-high-level/src/test/java/org/elasticsearch/client/TasksRequestConvertersTests.java

@@ -21,7 +21,6 @@ package org.elasticsearch.client;
 
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksRequest;
 import org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.test.ESTestCase;
@@ -36,12 +35,16 @@ import static org.hamcrest.Matchers.nullValue;
 public class TasksRequestConvertersTests extends ESTestCase {
 
     public void testCancelTasks() {
-        CancelTasksRequest request = new CancelTasksRequest();
         Map<String, String> expectedParams = new HashMap<>();
-        TaskId taskId = new TaskId(randomAlphaOfLength(5), randomNonNegativeLong());
-        TaskId parentTaskId = new TaskId(randomAlphaOfLength(5), randomNonNegativeLong());
-        request.setTaskId(taskId);
-        request.setParentTaskId(parentTaskId);
+        org.elasticsearch.client.tasks.TaskId taskId =
+            new org.elasticsearch.client.tasks.TaskId(randomAlphaOfLength(5), randomNonNegativeLong());
+        org.elasticsearch.client.tasks.TaskId parentTaskId =
+            new org.elasticsearch.client.tasks.TaskId(randomAlphaOfLength(5), randomNonNegativeLong());
+        org.elasticsearch.client.tasks.CancelTasksRequest request =
+            new org.elasticsearch.client.tasks.CancelTasksRequest.Builder()
+                .withTaskId(taskId)
+                .withParentTaskId(parentTaskId)
+                .build();
         expectedParams.put("task_id", taskId.toString());
         expectedParams.put("parent_task_id", parentTaskId.toString());
         Request httpRequest = TasksRequestConverters.cancelTasks(request);

+ 16 - 14
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/TasksClientDocumentationIT.java

@@ -23,14 +23,14 @@ import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.LatchedActionListener;
 import org.elasticsearch.action.TaskOperationFailure;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest;
-import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse;
 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.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.tasks.CancelTasksRequest;
+import org.elasticsearch.client.tasks.CancelTasksResponse;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.tasks.TaskInfo;
@@ -155,19 +155,21 @@ public class TasksClientDocumentationIT extends ESRestHighLevelClientTestCase {
         RestHighLevelClient client = highLevelClient();
         {
             // tag::cancel-tasks-request
-            CancelTasksRequest request = new CancelTasksRequest();
+            CancelTasksRequest request = new org.elasticsearch.client.tasks.CancelTasksRequest.Builder()
+                .withNodesFiltered(List.of("nodeId1", "nodeId2"))
+                .withActionsFiltered(List.of("cluster:*"))
+                .build();
             // end::cancel-tasks-request
 
             // tag::cancel-tasks-request-filter
-            request.setTaskId(new TaskId("nodeId1", 42)); //<1>
-            request.setActions("cluster:*"); // <2>
-            request.setNodes("nodeId1", "nodeId2"); // <3>
+            CancelTasksRequest byTaskIdRequest = new org.elasticsearch.client.tasks.CancelTasksRequest.Builder() // <1>
+                .withTaskId(new org.elasticsearch.client.tasks.TaskId("myNode",44L)) // <2>
+                .build(); // <3>
             // end::cancel-tasks-request-filter
 
         }
 
-        CancelTasksRequest request = new CancelTasksRequest();
-        request.setTaskId(TaskId.EMPTY_TASK_ID);
+        CancelTasksRequest request = new org.elasticsearch.client.tasks.CancelTasksRequest.Builder().build();
 
         // tag::cancel-tasks-execute
         CancelTasksResponse response = client.tasks().cancel(request, RequestOptions.DEFAULT);
@@ -176,18 +178,18 @@ public class TasksClientDocumentationIT extends ESRestHighLevelClientTestCase {
         assertThat(response, notNullValue());
 
         // tag::cancel-tasks-response-tasks
-        List<TaskInfo> tasks = response.getTasks(); // <1>
+        List<org.elasticsearch.client.tasks.TaskInfo> tasks = response.getTasks(); // <1>
         // end::cancel-tasks-response-tasks
 
         // tag::cancel-tasks-response-calc
-        Map<String, List<TaskInfo>> perNodeTasks = response.getPerNodeTasks(); // <1>
-        List<TaskGroup> groups = response.getTaskGroups(); // <2>
+        Map<String, List<org.elasticsearch.client.tasks.TaskInfo>> perNodeTasks = response.getPerNodeTasks(); // <1>
+        List<org.elasticsearch.client.tasks.TaskGroup> groups = response.getTaskGroups(); // <2>
         // end::cancel-tasks-response-calc
 
 
         // tag::cancel-tasks-response-failures
-        List<ElasticsearchException> nodeFailures = response.getNodeFailures(); // <1>
-        List<TaskOperationFailure> taskFailures = response.getTaskFailures(); // <2>
+        List<org.elasticsearch.client.tasks.ElasticsearchException> nodeFailures = response.getNodeFailures(); // <1>
+        List<org.elasticsearch.client.tasks.TaskOperationFailure> taskFailures = response.getTaskFailures(); // <2>
         // end::cancel-tasks-response-failures
 
         assertThat(response.getNodeFailures(), equalTo(emptyList()));
@@ -198,7 +200,7 @@ public class TasksClientDocumentationIT extends ESRestHighLevelClientTestCase {
 
         RestHighLevelClient client = highLevelClient();
         {
-            CancelTasksRequest request = new CancelTasksRequest();
+            CancelTasksRequest request = new org.elasticsearch.client.tasks.CancelTasksRequest.Builder().build();
 
             // tag::cancel-tasks-execute-listener
             ActionListener<CancelTasksResponse> listener =

+ 218 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/tasks/CancelTasksResponseTests.java

@@ -0,0 +1,218 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.TaskOperationFailure;
+import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse;
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.transport.TransportAddress;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.tasks.TaskId;
+import org.elasticsearch.tasks.TaskInfo;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+
+public class CancelTasksResponseTests extends AbstractResponseTestCase<CancelTasksResponseTests.ByNodeCancelTasksResponse,
+    org.elasticsearch.client.tasks.CancelTasksResponse> {
+
+    private static String NODE_ID = "node_id";
+
+    @Override
+    protected CancelTasksResponseTests.ByNodeCancelTasksResponse createServerTestInstance(XContentType xContentType) {
+        List<org.elasticsearch.tasks.TaskInfo> tasks = new ArrayList<>();
+        List<TaskOperationFailure> taskFailures = new ArrayList<>();
+        List<ElasticsearchException> nodeFailures = new ArrayList<>();
+
+        for (int i = 0; i < randomIntBetween(1, 4); i++) {
+            taskFailures.add(new TaskOperationFailure(randomAlphaOfLength(4), (long) i,
+                new RuntimeException(randomAlphaOfLength(4))));
+        }
+        for (int i = 0; i < randomIntBetween(1, 4); i++) {
+            nodeFailures.add(new ElasticsearchException(new RuntimeException(randomAlphaOfLength(10))));
+        }
+
+        for (int i = 0; i < 4; i++) {
+            tasks.add(new org.elasticsearch.tasks.TaskInfo(
+                new TaskId(NODE_ID, (long) i),
+                randomAlphaOfLength(4),
+                randomAlphaOfLength(4),
+                randomAlphaOfLength(10),
+                new FakeTaskStatus(randomAlphaOfLength(4), randomInt()),
+                randomLongBetween(1, 3),
+                randomIntBetween(5, 10),
+                false,
+                new TaskId("node1", randomLong()),
+                Map.of("x-header-of", "some-value")));
+        }
+
+        return new ByNodeCancelTasksResponse(tasks, taskFailures, nodeFailures);
+    }
+
+    @Override
+    protected org.elasticsearch.client.tasks.CancelTasksResponse doParseToClientInstance(XContentParser parser) throws IOException {
+        return org.elasticsearch.client.tasks.CancelTasksResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected void assertInstances(ByNodeCancelTasksResponse serverTestInstance,
+                                   org.elasticsearch.client.tasks.CancelTasksResponse clientInstance) {
+
+        // checking tasks
+        List<TaskInfo> sTasks = serverTestInstance.getTasks();
+        List<org.elasticsearch.client.tasks.TaskInfo> cTasks = clientInstance.getTasks();
+        Map<org.elasticsearch.client.tasks.TaskId, org.elasticsearch.client.tasks.TaskInfo> cTasksMap =
+            cTasks.stream().collect(Collectors.toMap(org.elasticsearch.client.tasks.TaskInfo::getTaskId,
+                Function.identity()));
+        for (TaskInfo ti : sTasks) {
+            org.elasticsearch.client.tasks.TaskInfo taskInfo = cTasksMap.get(
+                new org.elasticsearch.client.tasks.TaskId(ti.getTaskId().getNodeId(), ti.getTaskId().getId())
+            );
+            assertEquals(ti.getAction(), taskInfo.getAction());
+            assertEquals(ti.getDescription(), taskInfo.getDescription());
+            assertEquals(new HashMap<>(ti.getHeaders()), new HashMap<>(taskInfo.getHeaders()));
+            assertEquals(ti.getType(), taskInfo.getType());
+            assertEquals(ti.getStartTime(), taskInfo.getStartTime());
+            assertEquals(ti.getRunningTimeNanos(), taskInfo.getRunningTimeNanos());
+            assertEquals(ti.isCancellable(), taskInfo.isCancellable());
+            assertEquals(ti.getParentTaskId().getNodeId(), taskInfo.getParentTaskId().getNodeId());
+            assertEquals(ti.getParentTaskId().getId(), taskInfo.getParentTaskId().getId());
+            FakeTaskStatus status = (FakeTaskStatus) ti.getStatus();
+            assertEquals(status.code, taskInfo.getStatus().get("code"));
+            assertEquals(status.status, taskInfo.getStatus().get("status"));
+
+        }
+
+        //checking failures
+        List<ElasticsearchException> serverNodeFailures = serverTestInstance.getNodeFailures();
+        List<org.elasticsearch.client.tasks.ElasticsearchException> cNodeFailures = clientInstance.getNodeFailures();
+        List<String> sExceptionsMessages = serverNodeFailures.stream().map(x ->
+            org.elasticsearch.client.tasks.ElasticsearchException.buildMessage(
+                "exception", x.getMessage(), null)
+        ).collect(Collectors.toList()
+        );
+
+        List<String> cExceptionsMessages = cNodeFailures.stream().map(
+            org.elasticsearch.client.tasks.ElasticsearchException::getMsg
+        ).collect(Collectors.toList());
+        assertEquals(new HashSet<>(sExceptionsMessages), new HashSet<>(cExceptionsMessages));
+
+        List<TaskOperationFailure> sTaskFailures = serverTestInstance.getTaskFailures();
+        List<org.elasticsearch.client.tasks.TaskOperationFailure> cTaskFailures = clientInstance.getTaskFailures();
+
+        Map<Long, org.elasticsearch.client.tasks.TaskOperationFailure> cTasksFailuresMap =
+            cTaskFailures.stream().collect(Collectors.toMap(
+                org.elasticsearch.client.tasks.TaskOperationFailure::getTaskId,
+                Function.identity()));
+        for (TaskOperationFailure tof : sTaskFailures) {
+            org.elasticsearch.client.tasks.TaskOperationFailure failure = cTasksFailuresMap.get(tof.getTaskId());
+            assertEquals(tof.getNodeId(), failure.getNodeId());
+            assertTrue(failure.getReason().getMsg().contains("runtime_exception"));
+            assertTrue(failure.getStatus().contains("" + tof.getStatus().name()));
+        }
+    }
+
+    public static class FakeTaskStatus implements Task.Status {
+
+        final String status;
+        final int code;
+
+        public FakeTaskStatus(String status, int code) {
+            this.status = status;
+            this.code = code;
+        }
+
+        @Override
+        public String getWriteableName() {
+            return "faker";
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeString(status);
+            out.writeInt(code);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field("status", status);
+            builder.field("code", code);
+            return builder.endObject();
+        }
+    }
+
+    /**
+     * tasks are grouped under nodes, and in order to create DiscoveryNodes we need different
+     * IP addresses.
+     * So in this test we assume all tasks are issued by a single node whose name and IP address is hardcoded.
+     */
+    static class ByNodeCancelTasksResponse extends CancelTasksResponse {
+
+        ByNodeCancelTasksResponse(StreamInput in) throws IOException {
+            super(in);
+        }
+
+        ByNodeCancelTasksResponse(
+            List<TaskInfo> tasks,
+            List<TaskOperationFailure> taskFailures,
+            List<? extends ElasticsearchException> nodeFailures) {
+            super(tasks, taskFailures, nodeFailures);
+        }
+
+
+        // it knows the hardcoded address space.
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+
+            DiscoveryNodes.Builder dnBuilder = new DiscoveryNodes.Builder();
+            InetAddress inetAddress = InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, (byte) 0, (byte) 1});
+            TransportAddress transportAddress = new TransportAddress(inetAddress, randomIntBetween(0, 65535));
+
+            dnBuilder.add(new DiscoveryNode(NODE_ID, NODE_ID, transportAddress, emptyMap(), emptySet(), Version.CURRENT));
+
+            DiscoveryNodes build = dnBuilder.build();
+            builder.startObject();
+            super.toXContentGroupedByNode(builder, params, build);
+            builder.endObject();
+            return builder;
+        }
+    }
+}
+
+

+ 83 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/tasks/ElasticsearchExceptionTests.java

@@ -0,0 +1,83 @@
+/*
+ * 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.client.tasks;
+
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ElasticsearchExceptionTests extends AbstractResponseTestCase<org.elasticsearch.ElasticsearchException,
+    org.elasticsearch.client.tasks.ElasticsearchException> {
+
+    @Override
+    protected org.elasticsearch.ElasticsearchException createServerTestInstance(XContentType xContentType) {
+        IllegalStateException ies = new IllegalStateException("illegal_state");
+        IllegalArgumentException iae = new IllegalArgumentException("argument", ies);
+        org.elasticsearch.ElasticsearchException exception = new org.elasticsearch.ElasticsearchException("elastic_exception", iae);
+        exception.addHeader("key","value");
+        exception.addMetadata("es.meta","data");
+        exception.addSuppressed(new NumberFormatException("3/0"));
+        return exception;
+    }
+
+    @Override
+    protected ElasticsearchException doParseToClientInstance(XContentParser parser) throws IOException {
+        parser.nextToken();
+        return ElasticsearchException.fromXContent(parser);
+    }
+
+    @Override
+    protected void assertInstances(org.elasticsearch.ElasticsearchException serverTestInstance, ElasticsearchException clientInstance) {
+
+        IllegalArgumentException sCauseLevel1 = (IllegalArgumentException) serverTestInstance.getCause();
+        ElasticsearchException cCauseLevel1 = clientInstance.getCause();
+
+        assertTrue(sCauseLevel1 !=null);
+        assertTrue(cCauseLevel1 !=null);
+
+        IllegalStateException causeLevel2 = (IllegalStateException) serverTestInstance.getCause().getCause();
+        ElasticsearchException cCauseLevel2 = clientInstance.getCause().getCause();
+        assertTrue(causeLevel2 !=null);
+        assertTrue(cCauseLevel2 !=null);
+
+
+        ElasticsearchException cause = new ElasticsearchException(
+            "Elasticsearch exception [type=illegal_state_exception, reason=illegal_state]"
+        );
+        ElasticsearchException caused1 = new ElasticsearchException(
+            "Elasticsearch exception [type=illegal_argument_exception, reason=argument]",cause
+        );
+        ElasticsearchException caused2 = new ElasticsearchException(
+            "Elasticsearch exception [type=exception, reason=elastic_exception]",caused1
+        );
+
+        caused2.addHeader("key", List.of("value"));
+        ElasticsearchException supp = new ElasticsearchException(
+            "Elasticsearch exception [type=number_format_exception, reason=3/0]"
+        );
+        caused2.addSuppressed(List.of(supp));
+
+        assertEquals(caused2,clientInstance);
+
+    }
+
+}