Explorar el Código

HLRC: ML Close Job (#32943)

* HLRC: Adding ML Close Job API

HLRC: Adding ML Close Job API

* reconciling request converters

* Adding serialization tests and addressing PR comments

* Changing constructor order
Benjamin Trent hace 7 años
padre
commit
3fbaae10af

+ 26 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java

@@ -23,6 +23,8 @@ import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.elasticsearch.client.RequestConverters.EndpointBuilder;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.protocol.xpack.ml.CloseJobRequest;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest;
 import org.elasticsearch.protocol.xpack.ml.OpenJobRequest;
 import org.elasticsearch.protocol.xpack.ml.PutJobRequest;
@@ -61,6 +63,30 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request closeJob(CloseJobRequest closeJobRequest) {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("anomaly_detectors")
+            .addPathPart(Strings.collectionToCommaDelimitedString(closeJobRequest.getJobIds()))
+            .addPathPartAsIs("_close")
+            .build();
+        Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+
+        RequestConverters.Params params = new RequestConverters.Params(request);
+        if (closeJobRequest.isForce() != null) {
+            params.putParam("force", Boolean.toString(closeJobRequest.isForce()));
+        }
+        if (closeJobRequest.isAllowNoJobs() != null) {
+            params.putParam("allow_no_jobs", Boolean.toString(closeJobRequest.isAllowNoJobs()));
+        }
+        if (closeJobRequest.getTimeout() != null) {
+            params.putParam("timeout", closeJobRequest.getTimeout().getStringRep());
+        }
+
+        return request;
+    }
+
     static Request deleteJob(DeleteJobRequest deleteJobRequest) {
         String endpoint = new EndpointBuilder()
                 .addPathPartAsIs("_xpack")

+ 38 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java

@@ -19,6 +19,8 @@
 package org.elasticsearch.client;
 
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.protocol.xpack.ml.CloseJobRequest;
+import org.elasticsearch.protocol.xpack.ml.CloseJobResponse;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse;
 import org.elasticsearch.protocol.xpack.ml.OpenJobRequest;
@@ -166,4 +168,40 @@ public final class MachineLearningClient {
             listener,
             Collections.emptySet());
     }
+
+    /**
+     * Closes one or more Machine Learning Jobs. A job can be opened and closed multiple times throughout its lifecycle.
+     *
+     * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results.
+     *
+     * @param request request containing job_ids and additional options. See {@link CloseJobRequest}
+     * @param options  Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return response containing if the job was successfully closed or not.
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public CloseJobResponse closeJob(CloseJobRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::closeJob,
+            options,
+            CloseJobResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Closes one or more Machine Learning Jobs asynchronously, notifies listener on completion
+     *
+     * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results.
+     *
+     * @param request request containing job_ids and additional options. See {@link CloseJobRequest}
+     * @param options  Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener Listener to be notified upon request completion
+     */
+    public void closeJobAsync(CloseJobRequest request, RequestOptions options, ActionListener<CloseJobResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::closeJob,
+            options,
+            CloseJobResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
 }

+ 25 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java

@@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpPost;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.protocol.xpack.ml.CloseJobRequest;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest;
 import org.elasticsearch.protocol.xpack.ml.OpenJobRequest;
 import org.elasticsearch.protocol.xpack.ml.PutJobRequest;
@@ -66,6 +67,29 @@ public class MLRequestConvertersTests extends ESTestCase {
         assertEquals(bos.toString("UTF-8"), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}");
     }
 
+    public void testCloseJob() {
+        String jobId = "somejobid";
+        CloseJobRequest closeJobRequest = new CloseJobRequest(jobId);
+
+        Request request = MLRequestConverters.closeJob(closeJobRequest);
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_close", request.getEndpoint());
+        assertFalse(request.getParameters().containsKey("force"));
+        assertFalse(request.getParameters().containsKey("allow_no_jobs"));
+        assertFalse(request.getParameters().containsKey("timeout"));
+
+        closeJobRequest = new CloseJobRequest(jobId, "otherjobs*");
+        closeJobRequest.setForce(true);
+        closeJobRequest.setAllowNoJobs(false);
+        closeJobRequest.setTimeout(TimeValue.timeValueMinutes(10));
+        request = MLRequestConverters.closeJob(closeJobRequest);
+
+        assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + ",otherjobs*/_close", request.getEndpoint());
+        assertEquals(Boolean.toString(true), request.getParameters().get("force"));
+        assertEquals(Boolean.toString(false), request.getParameters().get("allow_no_jobs"));
+        assertEquals("10m", request.getParameters().get("timeout"));
+    }
+
     public void testDeleteJob() {
         String jobId = randomAlphaOfLength(10);
         DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId);
@@ -87,4 +111,4 @@ public class MLRequestConvertersTests extends ESTestCase {
         jobBuilder.setAnalysisConfig(analysisConfig);
         return jobBuilder.build();
     }
-}
+}

+ 15 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java

@@ -21,6 +21,8 @@ package org.elasticsearch.client;
 import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator;
 import org.apache.lucene.util.LuceneTestCase.AwaitsFix;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.protocol.xpack.ml.CloseJobRequest;
+import org.elasticsearch.protocol.xpack.ml.CloseJobResponse;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse;
 import org.elasticsearch.protocol.xpack.ml.OpenJobRequest;
@@ -77,6 +79,19 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         assertTrue(response.isOpened());
     }
 
+    public void testCloseJob() throws Exception {
+        String jobId = randomValidJobId();
+        Job job = buildJob(jobId);
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+        machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT);
+        machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT);
+
+        CloseJobResponse response = execute(new CloseJobRequest(jobId),
+            machineLearningClient::closeJob,
+            machineLearningClient::closeJobAsync);
+        assertTrue(response.isClosed());
+    }
+
     public static String randomValidJobId() {
         CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz0123456789".toCharArray());
         return generator.ofCodePointsLength(random(), 10, 10);

+ 54 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java

@@ -25,6 +25,8 @@ import org.elasticsearch.client.MachineLearningIT;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.protocol.xpack.ml.CloseJobRequest;
+import org.elasticsearch.protocol.xpack.ml.CloseJobResponse;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest;
 import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse;
 import org.elasticsearch.protocol.xpack.ml.OpenJobRequest;
@@ -221,4 +223,56 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
             assertTrue(latch.await(30L, TimeUnit.SECONDS));
         }
     }
+    
+    public void testCloseJob() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            Job job = MachineLearningIT.buildJob("closing-my-first-machine-learning-job");
+            client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT);
+            client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT);
+
+            //tag::x-pack-ml-close-job-request
+            CloseJobRequest closeJobRequest = new CloseJobRequest("closing-my-first-machine-learning-job", "otherjobs*"); //<1>
+            closeJobRequest.setForce(false); //<2>
+            closeJobRequest.setAllowNoJobs(true); //<3>
+            closeJobRequest.setTimeout(TimeValue.timeValueMinutes(10)); //<4>
+            //end::x-pack-ml-close-job-request
+
+            //tag::x-pack-ml-close-job-execute
+            CloseJobResponse closeJobResponse = client.machineLearning().closeJob(closeJobRequest, RequestOptions.DEFAULT);
+            boolean isClosed = closeJobResponse.isClosed(); //<1>
+            //end::x-pack-ml-close-job-execute
+
+        }
+        {
+            Job job = MachineLearningIT.buildJob("closing-my-second-machine-learning-job");
+            client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT);
+            client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT);
+
+            //tag::x-pack-ml-close-job-listener
+            ActionListener<CloseJobResponse> listener = new ActionListener<CloseJobResponse>() {
+                @Override
+                public void onResponse(CloseJobResponse closeJobResponse) {
+                    //<1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            //end::x-pack-ml-close-job-listener
+            CloseJobRequest closeJobRequest = new CloseJobRequest("closing-my-second-machine-learning-job");
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::x-pack-ml-close-job-execute-async
+            client.machineLearning().closeJobAsync(closeJobRequest, RequestOptions.DEFAULT, listener); //<1>
+            // end::x-pack-ml-close-job-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
 }

+ 59 - 0
docs/java-rest/high-level/ml/close-job.asciidoc

@@ -0,0 +1,59 @@
+[[java-rest-high-x-pack-ml-close-job]]
+=== Close Job API
+
+The Close Job API provides the ability to close {ml} jobs in the cluster.
+It accepts a `CloseJobRequest` object and responds
+with a `CloseJobResponse` object.
+
+[[java-rest-high-x-pack-ml-close-job-request]]
+==== Close Job Request
+
+A `CloseJobRequest` object gets created with an existing non-null `jobId`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-request]
+--------------------------------------------------
+<1> Constructing a new request referencing existing job IDs
+<2> Optionally used to close a failed job, or to forcefully close a job
+which has not responded to its initial close request.
+<3> Optionally set to ignore if a wildcard expression matches no jobs.
+ (This includes `_all` string or when no jobs have been specified)
+<4> Optionally setting the `timeout` value for how long the
+execution should wait for the job to be closed.
+
+[[java-rest-high-x-pack-ml-close-job-execution]]
+==== Execution
+
+The request can be executed through the `MachineLearningClient` contained
+in the `RestHighLevelClient` object, accessed via the `machineLearningClient()` method.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-execute]
+--------------------------------------------------
+<1> `isClosed()` from the `CloseJobResponse` indicates if the job was successfully
+closed or not.
+
+[[java-rest-high-x-pack-ml-close-job-execution-async]]
+==== Asynchronous Execution
+
+The request can also be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-execute-async]
+--------------------------------------------------
+<1> The `CloseJobRequest` to execute and the `ActionListener` to use when
+the execution completes
+
+The method does not block and returns immediately. The passed `ActionListener` is used
+to notify the caller of completion. A typical `ActionListener` for `CloseJobResponse` may
+look like
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-listener]
+--------------------------------------------------
+<1> `onResponse` is called back when the action is completed successfully
+<2> `onFailure` is called back when some unexpected error occurs

+ 1 - 1
docs/java-rest/high-level/ml/open-job.asciidoc

@@ -44,7 +44,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-open-job-exec
 the execution completes
 
 The method does not block and returns immediately. The passed `ActionListener` is used
-to notify the caller of completion. A typical `ActionListner` for `OpenJobResponse` may
+to notify the caller of completion. A typical `ActionListener` for `OpenJobResponse` may
 look like
 
 ["source","java",subs="attributes,callouts,macros"]

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -207,10 +207,12 @@ The Java High Level REST Client supports the following Machine Learning APIs:
 * <<java-rest-high-x-pack-ml-put-job>>
 * <<java-rest-high-x-pack-ml-delete-job>>
 * <<java-rest-high-x-pack-ml-open-job>>
+* <<java-rest-high-x-pack-ml-close-job>>
 
 include::ml/put-job.asciidoc[]
 include::ml/delete-job.asciidoc[]
 include::ml/open-job.asciidoc[]
+include::ml/close-job.asciidoc[]
 
 == Migration APIs
 

+ 191 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java

@@ -0,0 +1,191 @@
+/*
+ * 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.protocol.xpack.ml;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+public class CloseJobRequest extends ActionRequest implements ToXContentObject {
+
+    public static final ParseField JOB_IDS = new ParseField("job_ids");
+    public static final ParseField TIMEOUT = new ParseField("timeout");
+    public static final ParseField FORCE = new ParseField("force");
+    public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs");
+
+    @SuppressWarnings("unchecked")
+    public static final ConstructingObjectParser<CloseJobRequest, Void> PARSER = new ConstructingObjectParser<>(
+        "close_job_request",
+        true, a -> new CloseJobRequest((List<String>) a[0]));
+
+    static {
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), JOB_IDS);
+        PARSER.declareString((obj, val) -> obj.setTimeout(TimeValue.parseTimeValue(val, TIMEOUT.getPreferredName())), TIMEOUT);
+        PARSER.declareBoolean(CloseJobRequest::setForce, FORCE);
+        PARSER.declareBoolean(CloseJobRequest::setAllowNoJobs, ALLOW_NO_JOBS);
+    }
+
+    private static final String ALL_JOBS = "_all";
+
+    private final List<String> jobIds;
+    private TimeValue timeout;
+    private Boolean force;
+    private Boolean allowNoJobs;
+
+    /**
+     * Explicitly close all jobs
+     *
+     * @return a {@link CloseJobRequest} for all existing jobs
+     */
+    public static CloseJobRequest closeAllJobsRequest(){
+        return new CloseJobRequest(ALL_JOBS);
+    }
+
+    CloseJobRequest(List<String> jobIds) {
+        if (jobIds.isEmpty()) {
+            throw new InvalidParameterException("jobIds must not be empty");
+        }
+        if (jobIds.stream().anyMatch(Objects::isNull)) {
+            throw new NullPointerException("jobIds must not contain null values");
+        }
+        this.jobIds = new ArrayList<>(jobIds);
+    }
+
+    /**
+     * Close the specified Jobs via their unique jobIds
+     *
+     * @param jobIds must be non-null and non-empty and each jobId must be non-null
+     */
+    public CloseJobRequest(String... jobIds) {
+        this(Arrays.asList(jobIds));
+    }
+
+    /**
+     * All the jobIds to be closed
+     */
+    public List<String> getJobIds() {
+        return jobIds;
+    }
+
+    /**
+     * How long to wait for the close request to complete before timing out.
+     *
+     * Default: 30 minutes
+     */
+    public TimeValue getTimeout() {
+        return timeout;
+    }
+
+    /**
+     * {@link CloseJobRequest#getTimeout()}
+     */
+    public void setTimeout(TimeValue timeout) {
+        this.timeout = timeout;
+    }
+
+    /**
+     * Should the closing be forced.
+     *
+     * Use to close a failed job, or to forcefully close a job which has not responded to its initial close request.
+     */
+    public Boolean isForce() {
+        return force;
+    }
+
+    /**
+     * {@link CloseJobRequest#isForce()}
+     */
+    public void setForce(boolean force) {
+        this.force = force;
+    }
+
+    /**
+     * Whether to ignore if a wildcard expression matches no jobs.
+     *
+     * This includes `_all` string or when no jobs have been specified
+     */
+    public Boolean isAllowNoJobs() {
+        return allowNoJobs;
+    }
+
+    /**
+     * {@link CloseJobRequest#isAllowNoJobs()}
+     */
+    public void setAllowNoJobs(boolean allowNoJobs) {
+        this.allowNoJobs = allowNoJobs;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(jobIds, timeout, allowNoJobs, force);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        CloseJobRequest that = (CloseJobRequest) other;
+        return Objects.equals(jobIds, that.jobIds) &&
+            Objects.equals(timeout, that.timeout) &&
+            Objects.equals(allowNoJobs, that.allowNoJobs) &&
+            Objects.equals(force, that.force);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+
+        builder.field(JOB_IDS.getPreferredName(), jobIds);
+
+        if (timeout != null) {
+            builder.field(TIMEOUT.getPreferredName(), timeout.getStringRep());
+        }
+        if (force != null) {
+            builder.field(FORCE.getPreferredName(), force);
+        }
+        if (allowNoJobs != null) {
+            builder.field(ALLOW_NO_JOBS.getPreferredName(), allowNoJobs);
+        }
+
+        builder.endObject();
+        return builder;
+    }
+}

+ 89 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java

@@ -0,0 +1,89 @@
+/*
+ * 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.protocol.xpack.ml;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class CloseJobResponse extends ActionResponse implements ToXContentObject {
+
+    private static final ParseField CLOSED = new ParseField("closed");
+
+    public static final ObjectParser<CloseJobResponse, Void> PARSER =
+        new ObjectParser<>("close_job_response", true, CloseJobResponse::new);
+
+    static {
+        PARSER.declareBoolean(CloseJobResponse::setClosed, CLOSED);
+    }
+
+    private boolean closed;
+
+    CloseJobResponse() {
+    }
+
+    public CloseJobResponse(boolean closed) {
+        this.closed = closed;
+    }
+
+    public static CloseJobResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    public boolean isClosed() {
+        return closed;
+    }
+
+    public void setClosed(boolean closed) {
+        this.closed = closed;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        CloseJobResponse that = (CloseJobResponse) other;
+        return isClosed() == that.isClosed();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(isClosed());
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(CLOSED.getPreferredName(), closed);
+        builder.endObject();
+        return builder;
+    }
+}

+ 81 - 0
x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java

@@ -0,0 +1,81 @@
+/*
+ * 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.protocol.xpack.ml;
+
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CloseJobRequestTests extends AbstractXContentTestCase<CloseJobRequest> {
+
+    public void testCloseAllJobsRequest() {
+        CloseJobRequest request = CloseJobRequest.closeAllJobsRequest();
+        assertEquals(request.getJobIds().size(), 1);
+        assertEquals(request.getJobIds().get(0), "_all");
+    }
+
+    public void testWithNullJobIds() {
+        Exception exception = expectThrows(IllegalArgumentException.class, CloseJobRequest::new);
+        assertEquals(exception.getMessage(), "jobIds must not be empty");
+
+        exception = expectThrows(NullPointerException.class, () -> new CloseJobRequest("job1", null));
+        assertEquals(exception.getMessage(), "jobIds must not contain null values");
+    }
+
+
+    @Override
+    protected CloseJobRequest createTestInstance() {
+        int jobCount = randomIntBetween(1, 10);
+        List<String> jobIds = new ArrayList<>(jobCount);
+
+        for (int i = 0; i < jobCount; i++) {
+            jobIds.add(randomAlphaOfLength(10));
+        }
+
+        CloseJobRequest request = new CloseJobRequest(jobIds.toArray(new String[0]));
+
+        if (randomBoolean()) {
+            request.setAllowNoJobs(randomBoolean());
+        }
+
+        if (randomBoolean()) {
+            request.setTimeout(TimeValue.timeValueMinutes(randomIntBetween(1, 10)));
+        }
+
+        if (randomBoolean()) {
+            request.setForce(randomBoolean());
+        }
+
+        return request;
+    }
+
+    @Override
+    protected CloseJobRequest doParseInstance(XContentParser parser) throws IOException {
+        return CloseJobRequest.PARSER.parse(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}

+ 42 - 0
x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java

@@ -0,0 +1,42 @@
+/*
+ * 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.protocol.xpack.ml;
+
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+
+public class CloseJobResponseTests extends AbstractXContentTestCase<CloseJobResponse> {
+
+    @Override
+    protected CloseJobResponse createTestInstance() {
+        return new CloseJobResponse(randomBoolean());
+    }
+
+    @Override
+    protected CloseJobResponse doParseInstance(XContentParser parser) throws IOException {
+        return CloseJobResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}