Skip to content

Commit 10a1d27

Browse files
authored
[ML] Deleting a job now deletes the datafeed if necessary (#76010)
Previously attempting to delete a job that had a datafeed would return an exception. However, this was unnecessarily pedantic - the user would always want to delete both job and datafeed together, and would react by deleting the datafeed and then subsequently deleting the job again. This change makes the delete job API automatically delete a datafeed associated with the job. The same level of force is used for this delete datafeed request as was used on the delete job request. This means that it's possible to force-delete an open job with a started datafeed (since force-delete datafeed will automatically stop a started datafeed). It's still not possible to delete an opened job without using force.
1 parent 705a408 commit 10a1d27

File tree

5 files changed

+128
-31
lines changed

5 files changed

+128
-31
lines changed

docs/reference/ml/anomaly-detection/apis/delete-job.asciidoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ Deletes an existing {anomaly-job}.
1818

1919
* Requires the `manage_ml` cluster privilege. This privilege is included in the
2020
`machine_learning_admin` built-in role.
21-
* Before you can delete a job, you must delete the {dfeeds} that are associated
22-
with it. See <<ml-delete-datafeed>>.
2321
* Before you can delete a job, you must close it (unless you specify the `force`
2422
parameter). See <<ml-close-job>>.
2523

@@ -36,6 +34,10 @@ are granted to anyone over the `.ml-*` indices.
3634
It is not currently possible to delete multiple jobs using wildcards or a comma
3735
separated list.
3836

37+
If you delete a job that has a {dfeed}, the request will first attempt to
38+
delete the {dfeed}, as though <<ml-delete-datafeed>> was called with the same
39+
`timeout` and `force` parameters as this delete request.
40+
3941
[[ml-delete-job-path-parms]]
4042
== {api-path-parms-title}
4143

x-pack/plugin/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ def v7compatibilityNotSupportedTests = {
145145
'rollup/start_job/Test start job twice',
146146
'service_accounts/10_basic/Test service account tokens', // https://github.com/elastic/elasticsearch/pull/75200
147147

148+
// temporarily muted awaiting backport of https://github.com/elastic/elasticsearch/pull/76010
149+
'ml/delete_job_force/Test force delete job that is referred by a datafeed',
150+
'ml/jobs_crud/Test delete job that is referred by a datafeed',
151+
148152
// a type field was added to cat.ml_trained_models #73660, this is a backwards compatible change.
149153
// still this is a cat api, and we don't support them with rest api compatibility. (the test would be very hard to transform too)
150154
'ml/trained_model_cat_apis/Test cat trained models'

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

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import org.elasticsearch.tasks.TaskId;
3434
import org.elasticsearch.threadpool.ThreadPool;
3535
import org.elasticsearch.transport.TransportService;
36+
import org.elasticsearch.xpack.core.ClientHelper;
3637
import org.elasticsearch.xpack.core.ml.MlTasks;
38+
import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction;
3739
import org.elasticsearch.xpack.core.ml.action.DeleteJobAction;
3840
import org.elasticsearch.xpack.core.ml.action.KillProcessAction;
3941
import org.elasticsearch.xpack.core.ml.action.PutJobAction;
@@ -162,11 +164,20 @@ protected void masterOperation(Task task, DeleteJobAction.Request request, Clust
162164
},
163165
finalListener::onFailure);
164166

165-
ActionListener<Boolean> jobExistsListener = ActionListener.wrap(
167+
ActionListener<AcknowledgedResponse> datafeedDeleteListener = ActionListener.wrap(
166168
response -> {
167169
auditor.info(request.getJobId(), Messages.getMessage(Messages.JOB_AUDIT_DELETING, taskId));
168-
markJobAsDeletingIfNotUsed(request.getJobId(), taskId, markAsDeletingListener);
170+
cancelResetTaskIfExists(request.getJobId(), ActionListener.wrap(
171+
r -> jobConfigProvider.updateJobBlockReason(request.getJobId(), new Blocked(Blocked.Reason.DELETE, taskId),
172+
markAsDeletingListener),
173+
finalListener::onFailure
174+
));
169175
},
176+
finalListener::onFailure
177+
);
178+
179+
ActionListener<Boolean> jobExistsListener = ActionListener.wrap(
180+
response -> deleteDatafeedIfNecessary(request, datafeedDeleteListener),
170181
e -> {
171182
if (request.isForce()
172183
&& MlTasks.getJobTask(request.getJobId(), state.getMetadata().custom(PersistentTasksCustomMetadata.TYPE)) != null) {
@@ -223,23 +234,17 @@ private void forceDeleteJob(
223234
logger.debug(() -> new ParameterizedMessage("[{}] force deleting job", jobId));
224235

225236
// 3. Delete the job
226-
ActionListener<Boolean> removeTaskListener = new ActionListener<Boolean>() {
227-
@Override
228-
public void onResponse(Boolean response) {
229-
// use clusterService.state() here so that the updated state without the task is available
230-
normalDeleteJob(parentTaskClient, request, clusterService.state(), listener);
231-
}
232-
233-
@Override
234-
public void onFailure(Exception e) {
237+
ActionListener<Boolean> removeTaskListener = ActionListener.wrap(
238+
response -> normalDeleteJob(parentTaskClient, request, clusterService.state(), listener),
239+
e -> {
235240
if (ExceptionsHelper.unwrapCause(e) instanceof ResourceNotFoundException) {
236241
// use clusterService.state() here so that the updated state without the task is available
237242
normalDeleteJob(parentTaskClient, request, clusterService.state(), listener);
238243
} else {
239244
listener.onFailure(e);
240245
}
241246
}
242-
};
247+
);
243248

244249
// 2. Cancel the persistent task. This closes the process gracefully so
245250
// the process should be killed first.
@@ -288,21 +293,42 @@ private void checkJobIsNotOpen(String jobId, ClusterState state) {
288293
}
289294
}
290295

291-
private void markJobAsDeletingIfNotUsed(String jobId, TaskId taskId, ActionListener<PutJobAction.Response> listener) {
296+
private void deleteDatafeedIfNecessary(DeleteJobAction.Request deleteJobRequest, ActionListener<AcknowledgedResponse> listener) {
292297

293-
datafeedConfigProvider.findDatafeedIdsForJobIds(Collections.singletonList(jobId), ActionListener.wrap(
294-
datafeedIds -> {
295-
if (datafeedIds.isEmpty() == false) {
296-
listener.onFailure(ExceptionsHelper.conflictStatusException("Cannot delete job [" + jobId + "] because datafeed ["
297-
+ datafeedIds.iterator().next() + "] refers to it"));
298-
return;
299-
}
300-
cancelResetTaskIfExists(jobId, ActionListener.wrap(
301-
response -> jobConfigProvider.updateJobBlockReason(jobId, new Blocked(Blocked.Reason.DELETE, taskId), listener),
302-
listener::onFailure
303-
));
304-
},
305-
listener::onFailure
298+
datafeedConfigProvider.findDatafeedIdsForJobIds(Collections.singletonList(deleteJobRequest.getJobId()), ActionListener.wrap(
299+
datafeedIds -> {
300+
// Since it's only possible to delete a single job at a time there should not be more than one datafeed
301+
assert datafeedIds.size() <= 1 : "Expected at most 1 datafeed for a single job, got " + datafeedIds;
302+
if (datafeedIds.isEmpty()) {
303+
listener.onResponse(AcknowledgedResponse.TRUE);
304+
return;
305+
}
306+
DeleteDatafeedAction.Request deleteDatafeedRequest = new DeleteDatafeedAction.Request(datafeedIds.iterator().next());
307+
deleteDatafeedRequest.setForce(deleteJobRequest.isForce());
308+
deleteDatafeedRequest.timeout(deleteJobRequest.timeout());
309+
ClientHelper.executeAsyncWithOrigin(
310+
client,
311+
ClientHelper.ML_ORIGIN,
312+
DeleteDatafeedAction.INSTANCE,
313+
deleteDatafeedRequest,
314+
ActionListener.wrap(
315+
listener::onResponse,
316+
e -> {
317+
// It's possible that a simultaneous call to delete the datafeed has deleted it in between
318+
// us finding the datafeed ID and trying to delete it in this method - this is OK
319+
if (ExceptionsHelper.unwrapCause(e) instanceof ResourceNotFoundException) {
320+
listener.onResponse(AcknowledgedResponse.TRUE);
321+
} else {
322+
listener.onFailure(ExceptionsHelper.conflictStatusException(
323+
"failed to delete job [{}] as its datafeed [{}] could not be deleted", e,
324+
deleteJobRequest.getJobId(), deleteDatafeedRequest.getDatafeedId())
325+
);
326+
}
327+
}
328+
)
329+
);
330+
},
331+
listener::onFailure
306332
));
307333
}
308334

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/delete_job_force.yml

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ setup:
1616
}
1717
}
1818
19+
- do:
20+
headers:
21+
Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser
22+
indices.create:
23+
index: airline-data
24+
body:
25+
mappings:
26+
properties:
27+
time:
28+
type: date
29+
airline:
30+
type: keyword
31+
airport:
32+
type: text
33+
responsetime:
34+
type: float
35+
1936
---
2037
"Test force delete a closed job":
2138
- do:
@@ -65,11 +82,59 @@ setup:
6582
body: >
6683
{
6784
"job_id":"force-delete-job",
68-
"indexes":["index-foo"]
85+
"indices":["index-foo"]
6986
}
7087
- match: { datafeed_id: force-delete-job-datafeed }
7188

7289
- do:
73-
catch: /Cannot delete job \[force-delete-job\] because datafeed \[force-delete-job-datafeed\] refers to it/
7490
ml.delete_job:
7591
job_id: force-delete-job
92+
- match: { acknowledged: true }
93+
94+
- do:
95+
ml.get_jobs:
96+
job_id: "_all"
97+
- match: { count: 0 }
98+
99+
- do:
100+
ml.get_datafeeds:
101+
datafeed_id: "_all"
102+
- match: { count: 0 }
103+
104+
---
105+
"Test force delete an open job that is referred by a started datafeed":
106+
107+
- do:
108+
ml.open_job:
109+
job_id: force-delete-job
110+
111+
- do:
112+
ml.put_datafeed:
113+
datafeed_id: force-delete-job-started-datafeed
114+
body: >
115+
{
116+
"job_id":"force-delete-job",
117+
"indices":["airline-data"]
118+
}
119+
- match: { datafeed_id: force-delete-job-started-datafeed }
120+
121+
- do:
122+
ml.start_datafeed:
123+
datafeed_id: force-delete-job-started-datafeed
124+
start: 0
125+
126+
- do:
127+
ml.delete_job:
128+
force: true
129+
job_id: force-delete-job
130+
- match: { acknowledged: true }
131+
132+
- do:
133+
ml.get_jobs:
134+
job_id: "_all"
135+
- match: { count: 0 }
136+
137+
- do:
138+
ml.get_datafeeds:
139+
datafeed_id: "_all"
140+
- match: { count: 0 }

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_crud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,9 @@
623623
- match: { datafeed_id: "jobs-crud-test-datafeed-1" }
624624

625625
- do:
626-
catch: /Cannot delete job \[jobs-crud-datafeed-job\] because datafeed \[jobs-crud-test-datafeed-1\] refers to it/
627626
ml.delete_job:
628627
job_id: jobs-crud-datafeed-job
628+
- match: { acknowledged: true }
629629

630630
---
631631
"Test delete job that is opened":

0 commit comments

Comments
 (0)