1717import org .elasticsearch .action .TaskOperationFailure ;
1818import org .elasticsearch .action .support .ActionFilters ;
1919import org .elasticsearch .action .support .tasks .TransportTasksAction ;
20+ import org .elasticsearch .client .internal .Client ;
2021import org .elasticsearch .cluster .ClusterState ;
2122import org .elasticsearch .cluster .node .DiscoveryNode ;
2223import org .elasticsearch .cluster .node .DiscoveryNodes ;
2324import org .elasticsearch .cluster .service .ClusterService ;
2425import org .elasticsearch .common .util .concurrent .EsExecutors ;
26+ import org .elasticsearch .common .xcontent .XContentHelper ;
2527import org .elasticsearch .discovery .MasterNotDiscoveredException ;
28+ import org .elasticsearch .inference .TaskType ;
2629import org .elasticsearch .ingest .IngestMetadata ;
27- import org .elasticsearch .ingest .IngestService ;
2830import org .elasticsearch .injection .guice .Inject ;
2931import org .elasticsearch .rest .RestStatus ;
3032import org .elasticsearch .tasks .CancellableTask ;
3133import org .elasticsearch .tasks .Task ;
3234import org .elasticsearch .transport .TransportResponseHandler ;
3335import org .elasticsearch .transport .TransportService ;
36+ import org .elasticsearch .xcontent .XContentType ;
37+ import org .elasticsearch .xpack .core .inference .action .GetInferenceModelAction ;
3438import org .elasticsearch .xpack .core .ml .action .StopTrainedModelDeploymentAction ;
3539import org .elasticsearch .xpack .core .ml .inference .assignment .TrainedModelAssignment ;
3640import org .elasticsearch .xpack .core .ml .inference .assignment .TrainedModelAssignmentMetadata ;
@@ -63,7 +67,7 @@ public class TransportStopTrainedModelDeploymentAction extends TransportTasksAct
6367
6468 private static final Logger logger = LogManager .getLogger (TransportStopTrainedModelDeploymentAction .class );
6569
66- private final IngestService ingestService ;
70+ private final Client client ;
6771 private final TrainedModelAssignmentClusterService trainedModelAssignmentClusterService ;
6872 private final InferenceAuditor auditor ;
6973
@@ -72,7 +76,7 @@ public TransportStopTrainedModelDeploymentAction(
7276 ClusterService clusterService ,
7377 TransportService transportService ,
7478 ActionFilters actionFilters ,
75- IngestService ingestService ,
79+ Client client ,
7680 TrainedModelAssignmentClusterService trainedModelAssignmentClusterService ,
7781 InferenceAuditor auditor
7882 ) {
@@ -85,7 +89,7 @@ public TransportStopTrainedModelDeploymentAction(
8589 StopTrainedModelDeploymentAction .Response ::new ,
8690 EsExecutors .DIRECT_EXECUTOR_SERVICE
8791 );
88- this .ingestService = ingestService ;
92+ this .client = client ;
8993 this .trainedModelAssignmentClusterService = trainedModelAssignmentClusterService ;
9094 this .auditor = Objects .requireNonNull (auditor );
9195 }
@@ -154,21 +158,84 @@ protected void doExecute(
154158
155159 // NOTE, should only run on Master node
156160 assert clusterService .localNode ().isMasterNode ();
161+
162+ if (request .isForce () == false ) {
163+ checkIfUsedByInferenceEndpoint (
164+ request .getId (),
165+ ActionListener .wrap (canStop -> stopDeployment (task , request , maybeAssignment .get (), listener ), listener ::onFailure )
166+ );
167+ } else {
168+ stopDeployment (task , request , maybeAssignment .get (), listener );
169+ }
170+ }
171+
172+ private void stopDeployment (
173+ Task task ,
174+ StopTrainedModelDeploymentAction .Request request ,
175+ TrainedModelAssignment assignment ,
176+ ActionListener <StopTrainedModelDeploymentAction .Response > listener
177+ ) {
157178 trainedModelAssignmentClusterService .setModelAssignmentToStopping (
158179 request .getId (),
159- ActionListener .wrap (
160- setToStopping -> normalUndeploy (task , request .getId (), maybeAssignment .get (), request , listener ),
161- failure -> {
162- if (ExceptionsHelper .unwrapCause (failure ) instanceof ResourceNotFoundException ) {
163- listener .onResponse (new StopTrainedModelDeploymentAction .Response (true ));
164- return ;
165- }
166- listener .onFailure (failure );
180+ ActionListener .wrap (setToStopping -> normalUndeploy (task , request .getId (), assignment , request , listener ), failure -> {
181+ if (ExceptionsHelper .unwrapCause (failure ) instanceof ResourceNotFoundException ) {
182+ listener .onResponse (new StopTrainedModelDeploymentAction .Response (true ));
183+ return ;
167184 }
168- )
185+ listener .onFailure (failure );
186+ })
169187 );
170188 }
171189
190+ private void checkIfUsedByInferenceEndpoint (String deploymentId , ActionListener <Boolean > listener ) {
191+
192+ GetInferenceModelAction .Request getAllEndpoints = new GetInferenceModelAction .Request ("*" , TaskType .ANY );
193+ client .execute (GetInferenceModelAction .INSTANCE , getAllEndpoints , listener .delegateFailureAndWrap ((l , response ) -> {
194+ // filter by the ml node services
195+ var mlNodeEndpoints = response .getEndpoints ()
196+ .stream ()
197+ .filter (model -> model .getService ().equals ("elasticsearch" ) || model .getService ().equals ("elser" ))
198+ .toList ();
199+
200+ var endpointOwnsDeployment = mlNodeEndpoints .stream ()
201+ .filter (model -> model .getInferenceEntityId ().equals (deploymentId ))
202+ .findFirst ();
203+ if (endpointOwnsDeployment .isPresent ()) {
204+ l .onFailure (
205+ new ElasticsearchStatusException (
206+ "Cannot stop deployment [{}] as it was created by inference endpoint [{}]" ,
207+ RestStatus .CONFLICT ,
208+ deploymentId ,
209+ endpointOwnsDeployment .get ().getInferenceEntityId ()
210+ )
211+ );
212+ return ;
213+ }
214+
215+ // The inference endpoint may have been created by attaching to an existing deployment.
216+ for (var endpoint : mlNodeEndpoints ) {
217+ var serviceSettingsXContent = XContentHelper .toXContent (endpoint .getServiceSettings (), XContentType .JSON , false );
218+ var settingsMap = XContentHelper .convertToMap (serviceSettingsXContent , false , XContentType .JSON ).v2 ();
219+ // Endpoints with the deployment_id setting are attached to an existing deployment.
220+ var deploymentIdFromSettings = (String ) settingsMap .get ("deployment_id" );
221+ if (deploymentIdFromSettings != null && deploymentIdFromSettings .equals (deploymentId )) {
222+ // The endpoint was created to use this deployment
223+ l .onFailure (
224+ new ElasticsearchStatusException (
225+ "Cannot stop deployment [{}] as it is used by inference endpoint [{}]" ,
226+ RestStatus .CONFLICT ,
227+ deploymentId ,
228+ endpoint .getInferenceEntityId ()
229+ )
230+ );
231+ return ;
232+ }
233+ }
234+
235+ l .onResponse (true );
236+ }));
237+ }
238+
172239 private void redirectToMasterNode (
173240 DiscoveryNode masterNode ,
174241 StopTrainedModelDeploymentAction .Request request ,
0 commit comments