From 52ce02195a24be08e77d2c453c81638b7fda66bf Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 6 Sep 2018 15:38:13 -0500 Subject: [PATCH 1/2] HLRC: ML Delete Forecast API" --- .../client/MLRequestConverters.java | 21 +++ .../client/MachineLearningClient.java | 53 ++++++ .../client/ml/DeleteForecastRequest.java | 171 ++++++++++++++++++ .../client/MLRequestConvertersTests.java | 24 +++ .../client/MachineLearningIT.java | 19 ++ .../MlClientDocumentationIT.java | 60 ++++++ .../client/ml/DeleteForecastRequestTests.java | 55 ++++++ .../high-level/ml/delete-forecast.asciidoc | 78 ++++++++ .../high-level/supported-apis.asciidoc | 2 + 9 files changed, 483 insertions(+) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java create mode 100644 docs/java-rest/high-level/ml/delete-forecast.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index b8d977d8eeb94..88bd4ebfeb51a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -25,6 +25,7 @@ import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; @@ -160,6 +161,26 @@ static Request updateJob(UpdateJobRequest updateJobRequest) throws IOException { return request; } + static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(deleteForecastRequest.getJobId()) + .addPathPartAsIs("_forecast") + .addPathPart(deleteForecastRequest.getForecastId()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + if (deleteForecastRequest.isAllowNoForecasts() != null) { + params.putParam("allow_no_forecasts", Boolean.toString(deleteForecastRequest.isAllowNoForecasts())); + } + if (deleteForecastRequest.timeout() != null) { + params.putParam("timeout", deleteForecastRequest.timeout().getStringRep()); + } + return request; + } + static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index bdfc34ad997d6..7ab6a9b6821b5 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/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.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; @@ -361,6 +363,11 @@ public void flushJobAsync(FlushJobRequest request, RequestOptions options, Actio /** * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} * + *

+ * For additional info + * see + *

+ * * @param request the {@link UpdateJobRequest} object enclosing the desired updates * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return a PutJobResponse object containing the updated job object @@ -377,6 +384,10 @@ public PutJobResponse updateJob(UpdateJobRequest request, RequestOptions options /** * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} asynchronously * + *

+ * For additional info + * see + *

* @param request the {@link UpdateJobRequest} object enclosing the desired updates * @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 @@ -390,6 +401,48 @@ public void updateJobAsync(UpdateJobRequest request, RequestOptions options, Act Collections.emptySet()); } + /** + * Deletes a Machine Learning Job Forecast + * + *

+ * For additional info + * see + *

+ * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastID, and other options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return a AcknowledgedResponse object indicating request success + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public AcknowledgedResponse deleteForecast(DeleteForecastRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes a Machine Learning Job Forecast + * + *

+ * For additional info + * see + *

+ * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastID, and other options + * @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 deleteForecastAsync(DeleteForecastRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Gets the buckets for a Machine Learning Job. *

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java new file mode 100644 index 0000000000000..ac7085dab28b5 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java @@ -0,0 +1,171 @@ +/* + * 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.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.job.config.Job; +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.util.Objects; + +/** + * POJO for a delete forecast request + */ +public class DeleteForecastRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField FORECAST_ID = new ParseField("forecast_id"); + public static final ParseField ALLOW_NO_FORECASTS = new ParseField("allow_no_forecasts"); + public static final ParseField TIMEOUT = new ParseField("timeout"); + public static final String ALL = "_all"; + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("delete_forecast_request", (a) -> new DeleteForecastRequest((String) a[0])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); + PARSER.declareStringOrNull(DeleteForecastRequest::setForecastId, FORECAST_ID); + PARSER.declareBoolean(DeleteForecastRequest::setAllowNoForecasts, ALLOW_NO_FORECASTS); + PARSER.declareString(DeleteForecastRequest::timeout, TIMEOUT); + } + + /** + * Create a new {@link DeleteForecastRequest} that explicitly deletes all forecasts + * + * @param jobId the jobId of the Job whose forecasts to delete + */ + public static DeleteForecastRequest deleteAllForecasts(String jobId) { + DeleteForecastRequest request = new DeleteForecastRequest(jobId); + request.setForecastId(ALL); + return request; + } + + private final String jobId; + private String forecastId; + private Boolean allowNoForecasts; + private TimeValue timeout; + + /** + * Create a new DeleteForecastRequest for the given Job ID + * + * @param jobId the jobId of the Job whose forecast(s) to delete + */ + public DeleteForecastRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId, Job.ID.getPreferredName()); + } + + public String getJobId() { + return jobId; + } + + public String getForecastId() { + return forecastId; + } + + /** + * The forecast ID to delete. Can be also be {@link DeleteForecastRequest#ALL} to explicitly delete ALL forecasts + * + * @param forecastId forecast ID to delete or {@link DeleteForecastRequest#ALL} + */ + public void setForecastId(String forecastId) { + this.forecastId = forecastId; + } + + public Boolean isAllowNoForecasts() { + return allowNoForecasts; + } + + /** + * Sets the `allow_no_forecasts` field. + * + * @param allowNoForecasts when {@code true} no error is thrown when {@link DeleteForecastRequest#ALL} does not find any forecasts + */ + public void setAllowNoForecasts(boolean allowNoForecasts) { + this.allowNoForecasts = allowNoForecasts; + } + + /** + * Allows to set the timeout + * @param timeout timeout as a string (e.g. 1s) + */ + public void timeout(String timeout) { + this.timeout = TimeValue.parseTimeValue(timeout, this.timeout, getClass().getSimpleName() + ".timeout"); + } + + /** + * Allows to set the timeout + * @param timeout timeout as a {@link TimeValue} + */ + public void timeout(TimeValue timeout) { + this.timeout = timeout; + } + + public TimeValue timeout() { + return timeout; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + DeleteForecastRequest that = (DeleteForecastRequest) other; + return Objects.equals(jobId, that.jobId) && + Objects.equals(forecastId, that.forecastId) && + Objects.equals(allowNoForecasts, that.allowNoForecasts) && + Objects.equals(timeout, that.timeout); + } + + @Override + public int hashCode() { + return Objects.hash(jobId, forecastId, allowNoForecasts, timeout); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + if (forecastId != null) { + builder.field(FORECAST_ID.getPreferredName(), forecastId); + } + if (allowNoForecasts != null) { + builder.field(ALLOW_NO_FORECASTS.getPreferredName(), allowNoForecasts); + } + if (timeout != null) { + builder.field(TIMEOUT.getPreferredName(), timeout.getStringRep()); + } + builder.endObject(); + return builder; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index f1b035566aa4d..536edfc26e755 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/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.apache.http.client.methods.HttpPut; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; @@ -183,6 +184,29 @@ public void testUpdateJob() throws Exception { } } + public void testDeleteForecast() throws Exception { + String jobId = randomAlphaOfLength(10); + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId); + + Request request = MLRequestConverters.deleteForecast(deleteForecastRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_forecast", request.getEndpoint()); + assertFalse(request.getParameters().containsKey("timeout")); + assertFalse(request.getParameters().containsKey("allow_no_forecasts")); + + deleteForecastRequest.setForecastId(randomAlphaOfLength(10)); + deleteForecastRequest.timeout("10s"); + deleteForecastRequest.setAllowNoForecasts(true); + + request = MLRequestConverters.deleteForecast(deleteForecastRequest); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_forecast/" + deleteForecastRequest.getForecastId(), + request.getEndpoint()); + assertEquals("10s", + request.getParameters().get(DeleteForecastRequest.TIMEOUT.getPreferredName())); + assertEquals(Boolean.toString(true), + request.getParameters().get(DeleteForecastRequest.ALLOW_NO_FORECASTS.getPreferredName())); + } + public void testGetBuckets() throws IOException { String jobId = randomAlphaOfLength(10); GetBucketsRequest getBucketsRequest = new GetBucketsRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index bf25d9d1c0fb3..b09b7f0dddb3a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -20,6 +20,8 @@ import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.job.config.JobUpdate; import org.elasticsearch.common.unit.TimeValue; @@ -237,6 +239,23 @@ public void testUpdateJob() throws Exception { assertEquals("Updated description", getResponse.jobs().get(0).getDescription()); } + public void testDeleteForecast() throws Exception { + String jobId = "test-delete-forecast"; + + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT); + + // post data + // create forecast + String forecastId = ""; + DeleteForecastRequest request = new DeleteForecastRequest(jobId); + request.setForecastId(forecastId); + AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, machineLearningClient::deleteForecastAsync); + assertTrue(response.isAcknowledged()); + } + public static String randomValidJobId() { CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz0123456789".toCharArray()); return generator.ofCodePointsLength(random(), 10, 10); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index ac7835735fcf1..65fcae949ce2d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.MachineLearningGetResultsIT; import org.elasticsearch.client.MachineLearningIT; @@ -31,6 +32,7 @@ import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; @@ -631,8 +633,66 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testDeleteForecast() throws Exception { + RestHighLevelClient client = highLevelClient(); + + Job job = MachineLearningIT.buildJob("deleting-forecast-for-job"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); + //post data + //forecast + String forecastId = ""; + + { + //tag::x-pack-ml-delete-forecast-request + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest("deleting-forecast-for-job"); //<1> + //end::x-pack-ml-delete-forecast-request + + //tag::x-pack-ml-delete-forecast-request-options + deleteForecastRequest.setForecastId(forecastId); //<1> + deleteForecastRequest.timeout("30s"); //<2> + deleteForecastRequest.setAllowNoForecasts(true); //<3> + //end::x-pack-ml-delete-forecast-request-options + + //tag::x-pack-ml-delete-forecast-execute + AcknowledgedResponse deleteForecastResponse = client.machineLearning().deleteForecast(deleteForecastRequest, + RequestOptions.DEFAULT); + //end::x-pack-ml-delete-forecast-execute + + //tag::x-pack-ml-delete-forecast-response + boolean isAcknowledged = deleteForecastResponse.isAcknowledged(); //<1> + //end::x-pack-ml-delete-forecast-response + + } + { + //tag::x-pack-ml-delete-forecast-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse DeleteForecastResponse) { + //<1> + } + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-ml-delete-forecast-listener + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest("deleting-forecast-for-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-delete-forecast-execute-async + client.machineLearning().deleteForecastAsync(deleteForecastRequest, RequestOptions.DEFAULT, listener); //<1> + // end::x-pack-ml-delete-forecast-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetJobStats() throws Exception { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java new file mode 100644 index 0000000000000..4cae5cf112047 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java @@ -0,0 +1,55 @@ +/* + * 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.ml; + +import org.elasticsearch.client.ml.job.config.JobTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class DeleteForecastRequestTests extends AbstractXContentTestCase { + + @Override + protected DeleteForecastRequest createTestInstance() { + + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(JobTests.randomValidJobId()); + if (randomBoolean()) { + deleteForecastRequest.setForecastId(randomAlphaOfLength(10)); + } + if (randomBoolean()) { + deleteForecastRequest.setAllowNoForecasts(randomBoolean()); + } + if (randomBoolean()) { + deleteForecastRequest.timeout(randomTimeValue()); + } + return deleteForecastRequest; + } + + @Override + protected DeleteForecastRequest doParseInstance(XContentParser parser) throws IOException { + return DeleteForecastRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + +} diff --git a/docs/java-rest/high-level/ml/delete-forecast.asciidoc b/docs/java-rest/high-level/ml/delete-forecast.asciidoc new file mode 100644 index 0000000000000..4d72f088434ea --- /dev/null +++ b/docs/java-rest/high-level/ml/delete-forecast.asciidoc @@ -0,0 +1,78 @@ +[[java-rest-high-x-pack-ml-delete-forecast]] +=== Delete Forecast API + +The Delete Forecast API provides the ability to delete a {ml} job's +forecast in the cluster. +It accepts a `DeleteForecastRequest` object and responds +with an `AcknowledgedResponse` object. + +[[java-rest-high-x-pack-ml-delete-forecast-request]] +==== Delete Forecast Request + +A `DeleteForecastRequest` object gets created with an existing non-null `jobId`. +All other fields are optional for the request. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-request] +-------------------------------------------------- +<1> Constructing a new request referencing an existing `jobId` + +==== Optional Arguments + +The following arguments are optional. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-request-options] +-------------------------------------------------- +<1> Sets the specific forecastId to delete, can be set to `_all` to indicate ALL forecasts for the given +`jobId` +<2> Set the timeout for the request to respond, default is 30 seconds +<3> Set the `allow_no_forecasts` option. When `true` no error will be returned if an `_all` +request finds no forecasts. It defaults to `true` + +[[java-rest-high-x-pack-ml-delete-forecast-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-delete-forecast-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-delete-forecast-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-delete-forecast-execute-async] +-------------------------------------------------- +<1> The `DeleteForecastRequest` 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 `AcknowledgedResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs + +[[java-rest-high-x-pack-ml-delete-forecast-response]] +==== Flush Job Response + +An `AcknowledgedResponse` contains an acknowledgement of the forecast(s) deletion + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-response] +-------------------------------------------------- +<1> `isAcknowledged()` indicates if the forecast was successfully deleted or not. diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index c482c8bccff23..728dc82473b61 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -218,6 +218,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -231,6 +232,7 @@ include::ml/close-job.asciidoc[] include::ml/update-job.asciidoc[] include::ml/flush-job.asciidoc[] include::ml/get-job-stats.asciidoc[] +include::ml/delete-forecast.asciidoc[] include::ml/get-buckets.asciidoc[] include::ml/get-overall-buckets.asciidoc[] include::ml/get-records.asciidoc[] From e0a667a6b441f528ed3f5748bffbef11e656a005 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 7 Sep 2018 13:08:40 -0500 Subject: [PATCH 2/2] HLRC: ML Delete forecast api --- .../client/MLRequestConverters.java | 2 +- .../client/MachineLearningClient.java | 12 ++-- .../client/ml/DeleteForecastRequest.java | 38 +++++++---- .../client/MLRequestConvertersTests.java | 9 ++- .../client/MachineLearningIT.java | 68 +++++++++++++++++-- .../MlClientDocumentationIT.java | 33 +++++++-- .../client/ml/DeleteForecastRequestTests.java | 9 ++- .../high-level/ml/delete-forecast.asciidoc | 4 +- 8 files changed, 137 insertions(+), 38 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 2efae3783eba4..56e3c306444ea 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -188,7 +188,7 @@ static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throw .addPathPartAsIs("anomaly_detectors") .addPathPart(deleteForecastRequest.getJobId()) .addPathPartAsIs("_forecast") - .addPathPart(deleteForecastRequest.getForecastId()) + .addPathPart(Strings.collectionToCommaDelimitedString(deleteForecastRequest.getForecastIds())) .build(); Request request = new Request(HttpDelete.METHOD_NAME, endpoint); RequestConverters.Params params = new RequestConverters.Params(request); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 670d0af3df563..dbe281143006e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -450,14 +450,14 @@ public void updateJobAsync(UpdateJobRequest request, RequestOptions options, Act } /** - * Deletes a Machine Learning Job Forecast + * Deletes Machine Learning Job Forecasts * *

* For additional info - * see + * see *

* - * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastID, and other options + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return a AcknowledgedResponse object indicating request success * @throws IOException when there is a serialization issue sending the request or receiving the response @@ -471,14 +471,14 @@ public AcknowledgedResponse deleteForecast(DeleteForecastRequest request, Reques } /** - * Deletes a Machine Learning Job Forecast + * Deletes Machine Learning Job Forecasts asynchronously * *

* For additional info - * see + * see *

* - * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastID, and other options + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options * @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 */ diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java index ac7085dab28b5..f7c8a6c0733f8 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java @@ -22,12 +22,16 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; 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.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Objects; /** @@ -45,7 +49,8 @@ public class DeleteForecastRequest extends ActionRequest implements ToXContentOb static { PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); - PARSER.declareStringOrNull(DeleteForecastRequest::setForecastId, FORECAST_ID); + PARSER.declareStringOrNull( + (c, p) -> c.setForecastIds(Strings.commaDelimitedListToStringArray(p)), FORECAST_ID); PARSER.declareBoolean(DeleteForecastRequest::setAllowNoForecasts, ALLOW_NO_FORECASTS); PARSER.declareString(DeleteForecastRequest::timeout, TIMEOUT); } @@ -57,12 +62,12 @@ public class DeleteForecastRequest extends ActionRequest implements ToXContentOb */ public static DeleteForecastRequest deleteAllForecasts(String jobId) { DeleteForecastRequest request = new DeleteForecastRequest(jobId); - request.setForecastId(ALL); + request.setForecastIds(ALL); return request; } private final String jobId; - private String forecastId; + private List forecastIds = new ArrayList<>(); private Boolean allowNoForecasts; private TimeValue timeout; @@ -79,17 +84,24 @@ public String getJobId() { return jobId; } - public String getForecastId() { - return forecastId; + public List getForecastIds() { + return forecastIds; } /** - * The forecast ID to delete. Can be also be {@link DeleteForecastRequest#ALL} to explicitly delete ALL forecasts + * The forecast IDs to delete. Can be also be {@link DeleteForecastRequest#ALL} to explicitly delete ALL forecasts * - * @param forecastId forecast ID to delete or {@link DeleteForecastRequest#ALL} + * @param forecastIds forecast IDs to delete */ - public void setForecastId(String forecastId) { - this.forecastId = forecastId; + public void setForecastIds(String... forecastIds) { + setForecastIds(Arrays.asList(forecastIds)); + } + + void setForecastIds(List forecastIds) { + if (forecastIds.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException("forecastIds must not contain null values"); + } + this.forecastIds = new ArrayList<>(forecastIds); } public Boolean isAllowNoForecasts() { @@ -137,14 +149,14 @@ public boolean equals(Object other) { DeleteForecastRequest that = (DeleteForecastRequest) other; return Objects.equals(jobId, that.jobId) && - Objects.equals(forecastId, that.forecastId) && + Objects.equals(forecastIds, that.forecastIds) && Objects.equals(allowNoForecasts, that.allowNoForecasts) && Objects.equals(timeout, that.timeout); } @Override public int hashCode() { - return Objects.hash(jobId, forecastId, allowNoForecasts, timeout); + return Objects.hash(jobId, forecastIds, allowNoForecasts, timeout); } @Override @@ -156,8 +168,8 @@ public ActionRequestValidationException validate() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(Job.ID.getPreferredName(), jobId); - if (forecastId != null) { - builder.field(FORECAST_ID.getPreferredName(), forecastId); + if (forecastIds != null) { + builder.field(FORECAST_ID.getPreferredName(), Strings.collectionToCommaDelimitedString(forecastIds)); } if (allowNoForecasts != null) { builder.field(ALLOW_NO_FORECASTS.getPreferredName(), allowNoForecasts); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 3f4330f539704..2c14bd9d3dc81 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -44,6 +44,7 @@ import org.elasticsearch.client.ml.job.config.JobUpdate; import org.elasticsearch.client.ml.job.config.JobUpdateTests; import org.elasticsearch.client.ml.job.util.PageParams; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -214,12 +215,16 @@ public void testDeleteForecast() throws Exception { assertFalse(request.getParameters().containsKey("timeout")); assertFalse(request.getParameters().containsKey("allow_no_forecasts")); - deleteForecastRequest.setForecastId(randomAlphaOfLength(10)); + deleteForecastRequest.setForecastIds(randomAlphaOfLength(10), randomAlphaOfLength(10)); deleteForecastRequest.timeout("10s"); deleteForecastRequest.setAllowNoForecasts(true); request = MLRequestConverters.deleteForecast(deleteForecastRequest); - assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_forecast/" + deleteForecastRequest.getForecastId(), + assertEquals( + "/_xpack/ml/anomaly_detectors/" + + jobId + + "/_forecast/" + + Strings.collectionToCommaDelimitedString(deleteForecastRequest.getForecastIds()), request.getEndpoint()); assertEquals("10s", request.getParameters().get(DeleteForecastRequest.TIMEOUT.getPreferredName())); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 31ec7058d970f..db680aaa95d64 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -20,6 +20,8 @@ import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -298,13 +300,65 @@ public void testDeleteForecast() throws Exception { machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT); - // post data - // create forecast - String forecastId = ""; - DeleteForecastRequest request = new DeleteForecastRequest(jobId); - request.setForecastId(forecastId); - AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, machineLearningClient::deleteForecastAsync); - assertTrue(response.isAcknowledged()); + Job noForecastsJob = buildJob("test-delete-forecast-none"); + machineLearningClient.putJob(new PutJobRequest(noForecastsJob), RequestOptions.DEFAULT); + + PostDataRequest.JsonBuilder builder = new PostDataRequest.JsonBuilder(); + for(int i = 0; i < 30; i++) { + Map hashMap = new HashMap<>(); + hashMap.put("total", randomInt(1000)); + hashMap.put("timestamp", (i+1)*1000); + builder.addDoc(hashMap); + } + + PostDataRequest postDataRequest = new PostDataRequest(jobId, builder); + machineLearningClient.postData(postDataRequest, RequestOptions.DEFAULT); + machineLearningClient.flushJob(new FlushJobRequest(jobId), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse1 = machineLearningClient.forecastJob(new ForecastJobRequest(jobId), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse2 = machineLearningClient.forecastJob(new ForecastJobRequest(jobId), RequestOptions.DEFAULT); + waitForForecastToComplete(jobId, forecastJobResponse1.getForecastId()); + waitForForecastToComplete(jobId, forecastJobResponse2.getForecastId()); + + { + DeleteForecastRequest request = new DeleteForecastRequest(jobId); + request.setForecastIds(forecastJobResponse1.getForecastId(), forecastJobResponse2.getForecastId()); + AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, + machineLearningClient::deleteForecastAsync); + assertTrue(response.isAcknowledged()); + assertFalse(forecastExists(jobId, forecastJobResponse1.getForecastId())); + assertFalse(forecastExists(jobId, forecastJobResponse2.getForecastId())); + } + { + DeleteForecastRequest request = DeleteForecastRequest.deleteAllForecasts(noForecastsJob.getId()); + request.setAllowNoForecasts(true); + AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, + machineLearningClient::deleteForecastAsync); + assertTrue(response.isAcknowledged()); + } + { + DeleteForecastRequest request = DeleteForecastRequest.deleteAllForecasts(noForecastsJob.getId()); + request.setAllowNoForecasts(false); + ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, + () -> execute(request, machineLearningClient::deleteForecast, machineLearningClient::deleteForecastAsync)); + assertThat(exception.status().getStatus(), equalTo(404)); + } + } + + private void waitForForecastToComplete(String jobId, String forecastId) throws Exception { + GetRequest request = new GetRequest(".ml-anomalies-" + jobId); + request.id(jobId + "_model_forecast_request_stats_" + forecastId); + assertBusy(() -> { + GetResponse getResponse = highLevelClient().get(request, RequestOptions.DEFAULT); + assertTrue(getResponse.isExists()); + assertTrue(getResponse.getSourceAsString().contains("finished")); + }, 30, TimeUnit.SECONDS); + } + + private boolean forecastExists(String jobId, String forecastId) throws Exception { + GetRequest getRequest = new GetRequest(".ml-anomalies-" + jobId); + getRequest.id(jobId + "_model_forecast_request_stats_" + forecastId); + GetResponse getResponse = highLevelClient().get(getRequest, RequestOptions.DEFAULT); + return getResponse.isExists(); } public static String randomValidJobId() { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 4a723a2984a8a..33972e422a871 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -21,6 +21,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; @@ -645,9 +647,28 @@ public void testDeleteForecast() throws Exception { Job job = MachineLearningIT.buildJob("deleting-forecast-for-job"); client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); - //post data - //forecast - String forecastId = ""; + PostDataRequest.JsonBuilder builder = new PostDataRequest.JsonBuilder(); + for(int i = 0; i < 30; i++) { + Map hashMap = new HashMap<>(); + hashMap.put("total", randomInt(1000)); + hashMap.put("timestamp", (i+1)*1000); + builder.addDoc(hashMap); + } + + PostDataRequest postDataRequest = new PostDataRequest(job.getId(), builder); + client.machineLearning().postData(postDataRequest, RequestOptions.DEFAULT); + client.machineLearning().flushJob(new FlushJobRequest(job.getId()), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse = client.machineLearning(). + forecastJob(new ForecastJobRequest(job.getId()), RequestOptions.DEFAULT); + String forecastId = forecastJobResponse.getForecastId(); + + GetRequest request = new GetRequest(".ml-anomalies-" + job.getId()); + request.id(job.getId() + "_model_forecast_request_stats_" + forecastId); + assertBusy(() -> { + GetResponse getResponse = highLevelClient().get(request, RequestOptions.DEFAULT); + assertTrue(getResponse.isExists()); + assertTrue(getResponse.getSourceAsString().contains("finished")); + }, 30, TimeUnit.SECONDS); { //tag::x-pack-ml-delete-forecast-request @@ -655,7 +676,7 @@ public void testDeleteForecast() throws Exception { //end::x-pack-ml-delete-forecast-request //tag::x-pack-ml-delete-forecast-request-options - deleteForecastRequest.setForecastId(forecastId); //<1> + deleteForecastRequest.setForecastIds(forecastId); //<1> deleteForecastRequest.timeout("30s"); //<2> deleteForecastRequest.setAllowNoForecasts(true); //<3> //end::x-pack-ml-delete-forecast-request-options @@ -668,7 +689,6 @@ public void testDeleteForecast() throws Exception { //tag::x-pack-ml-delete-forecast-response boolean isAcknowledged = deleteForecastResponse.isAcknowledged(); //<1> //end::x-pack-ml-delete-forecast-response - } { //tag::x-pack-ml-delete-forecast-listener @@ -684,7 +704,8 @@ public void onFailure(Exception e) { } }; //end::x-pack-ml-delete-forecast-listener - DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest("deleting-forecast-for-job"); + DeleteForecastRequest deleteForecastRequest = DeleteForecastRequest.deleteAllForecasts(job.getId()); + deleteForecastRequest.setAllowNoForecasts(true); // Replace the empty listener by a blocking listener in test final CountDownLatch latch = new CountDownLatch(1); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java index 4cae5cf112047..ad01227771185 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java @@ -23,6 +23,8 @@ import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class DeleteForecastRequestTests extends AbstractXContentTestCase { @@ -31,7 +33,12 @@ protected DeleteForecastRequest createTestInstance() { DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(JobTests.randomValidJobId()); if (randomBoolean()) { - deleteForecastRequest.setForecastId(randomAlphaOfLength(10)); + int length = randomInt(10); + List ids = new ArrayList<>(length); + for(int i = 0; i < length; i++) { + ids.add(randomAlphaOfLength(10)); + } + deleteForecastRequest.setForecastIds(ids); } if (randomBoolean()) { deleteForecastRequest.setAllowNoForecasts(randomBoolean()); diff --git a/docs/java-rest/high-level/ml/delete-forecast.asciidoc b/docs/java-rest/high-level/ml/delete-forecast.asciidoc index 4d72f088434ea..09aa5c734ff1e 100644 --- a/docs/java-rest/high-level/ml/delete-forecast.asciidoc +++ b/docs/java-rest/high-level/ml/delete-forecast.asciidoc @@ -26,7 +26,7 @@ The following arguments are optional. -------------------------------------------------- include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-request-options] -------------------------------------------------- -<1> Sets the specific forecastId to delete, can be set to `_all` to indicate ALL forecasts for the given +<1> Sets the specific forecastIds to delete, can be set to `_all` to indicate ALL forecasts for the given `jobId` <2> Set the timeout for the request to respond, default is 30 seconds <3> Set the `allow_no_forecasts` option. When `true` no error will be returned if an `_all` @@ -67,7 +67,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-foreca <2> `onFailure` is called back when some unexpected error occurs [[java-rest-high-x-pack-ml-delete-forecast-response]] -==== Flush Job Response +==== Delete Forecast Response An `AcknowledgedResponse` contains an acknowledgement of the forecast(s) deletion