Skip to content

Commit e1b985e

Browse files
committed
[ML] Delete forecast API (#31134) (#33218)
* Delete forecast API (#31134)
1 parent 61f400e commit e1b985e

File tree

11 files changed

+656
-2
lines changed

11 files changed

+656
-2
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction;
5050
import org.elasticsearch.xpack.core.ml.action.DeleteExpiredDataAction;
5151
import org.elasticsearch.xpack.core.ml.action.DeleteFilterAction;
52+
import org.elasticsearch.xpack.core.ml.action.DeleteForecastAction;
5253
import org.elasticsearch.xpack.core.ml.action.DeleteJobAction;
5354
import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction;
5455
import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction;
@@ -253,6 +254,7 @@ public List<GenericAction> getClientActions() {
253254
UpdateProcessAction.INSTANCE,
254255
DeleteExpiredDataAction.INSTANCE,
255256
ForecastJobAction.INSTANCE,
257+
DeleteForecastAction.INSTANCE,
256258
GetCalendarsAction.INSTANCE,
257259
PutCalendarAction.INSTANCE,
258260
DeleteCalendarAction.INSTANCE,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.ml.action;
7+
8+
import org.elasticsearch.action.Action;
9+
import org.elasticsearch.action.ActionRequestBuilder;
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.action.support.master.AcknowledgedRequest;
12+
import org.elasticsearch.action.support.master.AcknowledgedResponse;
13+
import org.elasticsearch.client.ElasticsearchClient;
14+
import org.elasticsearch.common.io.stream.StreamInput;
15+
import org.elasticsearch.common.io.stream.StreamOutput;
16+
import org.elasticsearch.xpack.core.ml.job.config.Job;
17+
import org.elasticsearch.xpack.core.ml.job.results.ForecastRequestStats;
18+
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
19+
20+
import java.io.IOException;
21+
22+
public class DeleteForecastAction extends Action<AcknowledgedResponse> {
23+
24+
public static final DeleteForecastAction INSTANCE = new DeleteForecastAction();
25+
public static final String NAME = "cluster:admin/xpack/ml/job/forecast/delete";
26+
27+
private DeleteForecastAction() {
28+
super(NAME);
29+
}
30+
31+
@Override
32+
public AcknowledgedResponse newResponse() {
33+
return new AcknowledgedResponse();
34+
}
35+
36+
public static class Request extends AcknowledgedRequest<Request> {
37+
38+
private String jobId;
39+
private String forecastId;
40+
private boolean allowNoForecasts = true;
41+
42+
public Request() {
43+
}
44+
45+
public Request(String jobId, String forecastId) {
46+
this.jobId = ExceptionsHelper.requireNonNull(jobId, Job.ID.getPreferredName());
47+
this.forecastId = ExceptionsHelper.requireNonNull(forecastId, ForecastRequestStats.FORECAST_ID.getPreferredName());
48+
}
49+
50+
public String getJobId() {
51+
return jobId;
52+
}
53+
54+
public String getForecastId() {
55+
return forecastId;
56+
}
57+
58+
public boolean isAllowNoForecasts() {
59+
return allowNoForecasts;
60+
}
61+
62+
public void setAllowNoForecasts(boolean allowNoForecasts) {
63+
this.allowNoForecasts = allowNoForecasts;
64+
}
65+
66+
@Override
67+
public ActionRequestValidationException validate() {
68+
return null;
69+
}
70+
71+
@Override
72+
public void readFrom(StreamInput in) throws IOException {
73+
super.readFrom(in);
74+
jobId = in.readString();
75+
forecastId = in.readString();
76+
allowNoForecasts = in.readBoolean();
77+
}
78+
79+
@Override
80+
public void writeTo(StreamOutput out) throws IOException {
81+
super.writeTo(out);
82+
out.writeString(jobId);
83+
out.writeString(forecastId);
84+
out.writeBoolean(allowNoForecasts);
85+
}
86+
}
87+
88+
public static class RequestBuilder extends ActionRequestBuilder<Request, AcknowledgedResponse> {
89+
90+
public RequestBuilder(ElasticsearchClient client, DeleteForecastAction action) {
91+
super(client, action, new Request());
92+
}
93+
}
94+
95+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ public final class Messages {
161161
public static final String REST_JOB_NOT_CLOSED_REVERT = "Can only revert to a model snapshot when the job is closed.";
162162
public static final String REST_NO_SUCH_MODEL_SNAPSHOT = "No model snapshot with id [{0}] exists for job [{1}]";
163163
public static final String REST_START_AFTER_END = "Invalid time range: end time ''{0}'' is earlier than start time ''{1}''.";
164-
164+
public static final String REST_NO_SUCH_FORECAST = "No forecast(s) [{0}] exists for job [{1}]";
165+
public static final String REST_CANNOT_DELETE_FORECAST_IN_CURRENT_STATE =
166+
"Forecast(s) [{0}] for job [{1}] needs to be either FAILED or FINISHED to be deleted";
165167
public static final String FIELD_CANNOT_BE_NULL = "Field [{0}] cannot be null";
166168

167169
private Messages() {

x-pack/plugin/ml/qa/ml-with-security/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ integTestRunner {
9191
'ml/validate/Test invalid job config',
9292
'ml/validate/Test job config is invalid because model snapshot id set',
9393
'ml/validate/Test job config that is invalid only because of the job ID',
94-
'ml/validate_detector/Test invalid detector'
94+
'ml/validate_detector/Test invalid detector',
95+
'ml/delete_forecast/Test delete on _all forecasts not allow no forecasts',
96+
'ml/delete_forecast/Test delete forecast on missing forecast'
9597
].join(',')
9698
}
9799

x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
import org.elasticsearch.ElasticsearchException;
99
import org.elasticsearch.ElasticsearchStatusException;
10+
import org.elasticsearch.action.support.master.AcknowledgedResponse;
11+
import org.elasticsearch.cluster.metadata.MetaData;
1012
import org.elasticsearch.common.unit.TimeValue;
13+
import org.elasticsearch.xpack.core.ml.action.DeleteForecastAction;
1114
import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig;
1215
import org.elasticsearch.xpack.core.ml.job.config.AnalysisLimits;
1316
import org.elasticsearch.xpack.core.ml.job.config.DataDescription;
@@ -276,6 +279,104 @@ public void testOverflowToDisk() throws Exception {
276279

277280
}
278281

282+
public void testDelete() throws Exception {
283+
Detector.Builder detector = new Detector.Builder("mean", "value");
284+
285+
TimeValue bucketSpan = TimeValue.timeValueHours(1);
286+
AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(Collections.singletonList(detector.build()));
287+
analysisConfig.setBucketSpan(bucketSpan);
288+
DataDescription.Builder dataDescription = new DataDescription.Builder();
289+
dataDescription.setTimeFormat("epoch");
290+
291+
Job.Builder job = new Job.Builder("forecast-it-test-delete");
292+
job.setAnalysisConfig(analysisConfig);
293+
job.setDataDescription(dataDescription);
294+
295+
registerJob(job);
296+
putJob(job);
297+
openJob(job.getId());
298+
299+
long now = Instant.now().getEpochSecond();
300+
long timestamp = now - 50 * bucketSpan.seconds();
301+
List<String> data = new ArrayList<>();
302+
while (timestamp < now) {
303+
data.add(createJsonRecord(createRecord(timestamp, 10.0)));
304+
data.add(createJsonRecord(createRecord(timestamp, 30.0)));
305+
timestamp += bucketSpan.seconds();
306+
}
307+
308+
postData(job.getId(), data.stream().collect(Collectors.joining()));
309+
flushJob(job.getId(), false);
310+
String forecastIdDefaultDurationDefaultExpiry = forecast(job.getId(), null, null);
311+
String forecastIdDuration1HourNoExpiry = forecast(job.getId(), TimeValue.timeValueHours(1), TimeValue.ZERO);
312+
waitForecastToFinish(job.getId(), forecastIdDefaultDurationDefaultExpiry);
313+
waitForecastToFinish(job.getId(), forecastIdDuration1HourNoExpiry);
314+
closeJob(job.getId());
315+
316+
{
317+
ForecastRequestStats forecastStats = getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry);
318+
assertNotNull(forecastStats);
319+
ForecastRequestStats otherStats = getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry);
320+
assertNotNull(otherStats);
321+
}
322+
323+
{
324+
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(),
325+
forecastIdDefaultDurationDefaultExpiry + "," + forecastIdDuration1HourNoExpiry);
326+
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
327+
assertTrue(response.isAcknowledged());
328+
}
329+
330+
{
331+
ForecastRequestStats forecastStats = getForecastStats(job.getId(), forecastIdDefaultDurationDefaultExpiry);
332+
assertNull(forecastStats);
333+
ForecastRequestStats otherStats = getForecastStats(job.getId(), forecastIdDuration1HourNoExpiry);
334+
assertNull(otherStats);
335+
}
336+
337+
{
338+
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(), "forecast-does-not-exist");
339+
ElasticsearchException e = expectThrows(ElasticsearchException.class,
340+
() -> client().execute(DeleteForecastAction.INSTANCE, request).actionGet());
341+
assertThat(e.getMessage(),
342+
equalTo("No forecast(s) [forecast-does-not-exist] exists for job [forecast-it-test-delete]"));
343+
}
344+
345+
{
346+
DeleteForecastAction.Request request = new DeleteForecastAction.Request(job.getId(), MetaData.ALL);
347+
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
348+
assertTrue(response.isAcknowledged());
349+
}
350+
351+
{
352+
Job.Builder otherJob = new Job.Builder("forecasts-delete-with-all-and-allow-no-forecasts");
353+
otherJob.setAnalysisConfig(analysisConfig);
354+
otherJob.setDataDescription(dataDescription);
355+
356+
registerJob(otherJob);
357+
putJob(otherJob);
358+
DeleteForecastAction.Request request = new DeleteForecastAction.Request(otherJob.getId(), MetaData.ALL);
359+
AcknowledgedResponse response = client().execute(DeleteForecastAction.INSTANCE, request).actionGet();
360+
assertTrue(response.isAcknowledged());
361+
}
362+
363+
{
364+
Job.Builder otherJob = new Job.Builder("forecasts-delete-with-all-and-not-allow-no-forecasts");
365+
otherJob.setAnalysisConfig(analysisConfig);
366+
otherJob.setDataDescription(dataDescription);
367+
368+
registerJob(otherJob);
369+
putJob(otherJob);
370+
371+
DeleteForecastAction.Request request = new DeleteForecastAction.Request(otherJob.getId(), MetaData.ALL);
372+
request.setAllowNoForecasts(false);
373+
ElasticsearchException e = expectThrows(ElasticsearchException.class,
374+
() -> client().execute(DeleteForecastAction.INSTANCE, request).actionGet());
375+
assertThat(e.getMessage(),
376+
equalTo("No forecast(s) [_all] exists for job [forecasts-delete-with-all-and-not-allow-no-forecasts]"));
377+
}
378+
}
379+
279380
private void createDataWithLotsOfClientIps(TimeValue bucketSpan, Job.Builder job) throws IOException {
280381
long now = Instant.now().getEpochSecond();
281382
long timestamp = now - 15 * bucketSpan.seconds();

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction;
6464
import org.elasticsearch.xpack.core.ml.action.DeleteExpiredDataAction;
6565
import org.elasticsearch.xpack.core.ml.action.DeleteFilterAction;
66+
import org.elasticsearch.xpack.core.ml.action.DeleteForecastAction;
6667
import org.elasticsearch.xpack.core.ml.action.DeleteJobAction;
6768
import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction;
6869
import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction;
@@ -115,6 +116,7 @@
115116
import org.elasticsearch.xpack.ml.action.TransportDeleteDatafeedAction;
116117
import org.elasticsearch.xpack.ml.action.TransportDeleteExpiredDataAction;
117118
import org.elasticsearch.xpack.ml.action.TransportDeleteFilterAction;
119+
import org.elasticsearch.xpack.ml.action.TransportDeleteForecastAction;
118120
import org.elasticsearch.xpack.ml.action.TransportDeleteJobAction;
119121
import org.elasticsearch.xpack.ml.action.TransportDeleteModelSnapshotAction;
120122
import org.elasticsearch.xpack.ml.action.TransportFinalizeJobExecutionAction;
@@ -201,6 +203,7 @@
201203
import org.elasticsearch.xpack.ml.rest.filter.RestPutFilterAction;
202204
import org.elasticsearch.xpack.ml.rest.filter.RestUpdateFilterAction;
203205
import org.elasticsearch.xpack.ml.rest.job.RestCloseJobAction;
206+
import org.elasticsearch.xpack.ml.rest.job.RestDeleteForecastAction;
204207
import org.elasticsearch.xpack.ml.rest.job.RestDeleteJobAction;
205208
import org.elasticsearch.xpack.ml.rest.job.RestFlushJobAction;
206209
import org.elasticsearch.xpack.ml.rest.job.RestForecastJobAction;
@@ -494,6 +497,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
494497
new RestDeleteModelSnapshotAction(settings, restController),
495498
new RestDeleteExpiredDataAction(settings, restController),
496499
new RestForecastJobAction(settings, restController),
500+
new RestDeleteForecastAction(settings, restController),
497501
new RestGetCalendarsAction(settings, restController),
498502
new RestPutCalendarAction(settings, restController),
499503
new RestDeleteCalendarAction(settings, restController),
@@ -550,6 +554,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
550554
new ActionHandler<>(UpdateProcessAction.INSTANCE, TransportUpdateProcessAction.class),
551555
new ActionHandler<>(DeleteExpiredDataAction.INSTANCE, TransportDeleteExpiredDataAction.class),
552556
new ActionHandler<>(ForecastJobAction.INSTANCE, TransportForecastJobAction.class),
557+
new ActionHandler<>(DeleteForecastAction.INSTANCE, TransportDeleteForecastAction.class),
553558
new ActionHandler<>(GetCalendarsAction.INSTANCE, TransportGetCalendarsAction.class),
554559
new ActionHandler<>(PutCalendarAction.INSTANCE, TransportPutCalendarAction.class),
555560
new ActionHandler<>(DeleteCalendarAction.INSTANCE, TransportDeleteCalendarAction.class),

0 commit comments

Comments
 (0)