Skip to content

Commit

Permalink
support force stop
Browse files Browse the repository at this point in the history
  • Loading branch information
darnautov committed Nov 15, 2021
1 parent 86c265f commit 1b1a3b2
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 4 deletions.
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="Force 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.confirmText"
defaultMessage="Selected model has associated pipelines: "
/>
<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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('check_capabilities', () => {
);
const { capabilities } = await getCapabilities();
const count = Object.keys(capabilities).length;
expect(count).toBe(31);
expect(count).toBe(32);
});
});

Expand Down Expand Up @@ -101,6 +101,7 @@ describe('check_capabilities', () => {
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateMlAlerts).toBe(false);
expect(capabilities.canViewMlNodes).toBe(false);
});

test('full capabilities', async () => {
Expand Down Expand Up @@ -146,6 +147,7 @@ describe('check_capabilities', () => {
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(true);
expect(capabilities.canViewMlNodes).toBe(true);
});

test('upgrade in progress with full capabilities', async () => {
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 @@ -262,6 +263,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 @@ -272,6 +274,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 1b1a3b2

Please sign in to comment.