Skip to content

Commit

Permalink
[ML] Support force stop deployment (#118563)
Browse files Browse the repository at this point in the history
* support force stop

* fix i18n, revert tests

* update text

* remove "Force" from the confirmation modals
  • Loading branch information
darnautov authored Nov 25, 2021
1 parent f993b04 commit 8aec972
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export const CloseJobConfirm: FC<Props> = ({
confirmButtonText={
combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING
? i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.stopAndClose.button', {
defaultMessage: 'Force stop and close',
defaultMessage: 'Stop and close',
})
: i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.close.button', {
defaultMessage: 'Force close',
defaultMessage: 'Close',
})
}
defaultFocusedButton="confirm"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const StopActionModal: FC<StopAction> = ({ closeModal, item, forceStopAnd
confirmButtonText={i18n.translate(
'xpack.ml.dataframe.analyticsList.forceStopModalStartButton',
{
defaultMessage: 'Force stop',
defaultMessage: 'Stop',
}
)}
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,13 @@ export function trainedModelsApiProvider(httpService: HttpService) {
});
},

stopModelAllocation(modelId: string) {
stopModelAllocation(modelId: string, options: { force: boolean } = { force: false }) {
const force = options?.force;

return httpService.http<{ acknowledge: boolean }>({
path: `${apiBasePath}/trained_models/${modelId}/deployment/_stop`,
method: 'POST',
query: { force },
});
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FC } from 'react';
import { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { OverlayStart } from 'kibana/public';
import type { ModelItem } from './models_list';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';

interface ForceStopModelConfirmDialogProps {
model: ModelItem;
onCancel: () => void;
onConfirm: () => void;
}

export const ForceStopModelConfirmDialog: FC<ForceStopModelConfirmDialogProps> = ({
model,
onConfirm,
onCancel,
}) => {
return (
<EuiConfirmModal
title={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.forceStopDialog.title"
defaultMessage="Stop model {modelId}?"
values={{ modelId: model.model_id }}
/>
}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.forceStopDialog.cancelText"
defaultMessage="Cancel"
/>
}
confirmButtonText={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.forceStopDialog.confirmText"
defaultMessage="Stop"
/>
}
buttonColor="danger"
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.forceStopDialog.pipelinesWarning"
defaultMessage="You can't use these ingest pipelines until you restart the model:"
/>
<ul>
{Object.keys(model.pipelines!)
.sort()
.map((pipelineName) => {
return <li key={pipelineName}>{pipelineName}</li>;
})}
</ul>
</EuiConfirmModal>
);
};

export const getUserConfirmationProvider =
(overlays: OverlayStart) => async (forceStopModel: ModelItem) => {
return new Promise(async (resolve, reject) => {
try {
const modalSession = overlays.openModal(
toMountPoint(
<ForceStopModelConfirmDialog
model={forceStopModel}
onCancel={() => {
modalSession.close();
resolve(false);
}}
onConfirm={() => {
modalSession.close();
resolve(true);
}}
/>
)
);
} catch (e) {
resolve(false);
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
import { useRefresh } from '../../routing/use_refresh';
import { DEPLOYMENT_STATE } from '../../../../common/constants/trained_models';
import { getUserConfirmationProvider } from './force_stop_dialog';

type Stats = Omit<TrainedModelStat, 'model_id'>;

Expand Down Expand Up @@ -80,6 +81,7 @@ export const ModelsList: FC = () => {
const {
services: {
application: { navigateToUrl, capabilities },
overlays,
},
} = useMlKibana();
const urlLocator = useMlLocator()!;
Expand Down Expand Up @@ -110,6 +112,8 @@ export const ModelsList: FC = () => {
{}
);

const getUserConfirmation = useMemo(() => getUserConfirmationProvider(overlays), []);

const navigateToPath = useNavigateToPath();

const isBuiltInModel = useCallback(
Expand Down Expand Up @@ -418,13 +422,21 @@ export const ModelsList: FC = () => {
available: (item) => item.model_type === 'pytorch',
enabled: (item) =>
!isLoading &&
!isPopulatedObject(item.pipelines) &&
isPopulatedObject(item.stats?.deployment_stats) &&
item.stats?.deployment_stats?.state !== DEPLOYMENT_STATE.STOPPING,
onClick: async (item) => {
const requireForceStop = isPopulatedObject(item.pipelines);

if (requireForceStop) {
const hasUserApproved = await getUserConfirmation(item);
if (!hasUserApproved) return;
}

try {
setIsLoading(true);
await trainedModelsApiService.stopModelAllocation(item.model_id);
await trainedModelsApiService.stopModelAllocation(item.model_id, {
force: requireForceStop,
});
displaySuccessToast(
i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', {
defaultMessage: 'Deployment for "{modelId}" has been stopped successfully.',
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/ml/server/routes/trained_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { modelsProvider } from '../models/data_frame_analytics';
import { TrainedModelConfigResponse } from '../../common/types/trained_models';
import { memoryOverviewServiceProvider } from '../models/memory_overview';
import { mlLog } from '../lib/log';
import { forceQuerySchema } from './schemas/anomaly_detectors_schema';

export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) {
/**
Expand Down Expand Up @@ -266,6 +267,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
path: '/api/ml/trained_models/{modelId}/deployment/_stop',
validate: {
params: modelIdSchema,
query: forceQuerySchema,
},
options: {
tags: ['access:ml:canGetDataFrameAnalytics'],
Expand All @@ -276,6 +278,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
const { modelId } = request.params;
const { body } = await mlClient.stopTrainedModelDeployment({
model_id: modelId,
force: request.query.force ?? false,
});
return response.ok({
body,
Expand Down

0 comments on commit 8aec972

Please sign in to comment.