[ML] Job deleting optimisations (#29848)

* [ML] Job deleting optimisations

* fixing force=true

* updating deleting jobs check
This commit is contained in:
James Gowdy 2019-02-05 12:29:01 +00:00 committed by GitHub
parent fda6efed1a
commit 2850fd6735
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 163 additions and 68 deletions

View file

@ -21,3 +21,16 @@ export function renderTemplate(str, data) {
return str;
}
export function stringHash(str) {
let hash = 0;
let chr = '';
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash < 0 ? hash * -2 : hash;
}

View file

@ -67,10 +67,12 @@ export const DeleteJobModal = injectI18n(class extends Component {
deleteJob = () => {
this.setState({ deleting: true });
deleteJobs(this.state.jobs, () => {
deleteJobs(this.state.jobs);
setTimeout(() => {
this.refreshJobs();
this.closeModal();
});
}, 500);
this.closeModal();
}
setEL = (el) => {

View file

@ -35,7 +35,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
defaultMessage: 'Start datafeed'
}),
icon: 'play',
enabled: () => (canStartStopDatafeed),
enabled: (item) => (item.deleting !== true && canStartStopDatafeed),
available: (item) => (isStartable([item])),
onClick: (item) => {
showStartDatafeedModal([item]);
@ -49,7 +49,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
defaultMessage: 'Stop datafeed'
}),
icon: 'stop',
enabled: () => (canStartStopDatafeed),
enabled: (item) => (item.deleting !== true && canStartStopDatafeed),
available: (item) => (isStoppable([item])),
onClick: (item) => {
stopDatafeeds([item], refreshJobs);
@ -63,7 +63,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
defaultMessage: 'Close job'
}),
icon: 'cross',
enabled: () => (canCloseJob),
enabled: (item) => (item.deleting !== true && canCloseJob),
available: (item) => (isClosable([item])),
onClick: (item) => {
closeJobs([item], refreshJobs);
@ -87,7 +87,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
return indexPatternNames.some(ipName => ipName === dfiName);
});
return canCreateJob && jobIndicesAvailable;
return (item.deleting !== true && canCreateJob && jobIndicesAvailable);
},
onClick: (item) => {
cloneJob(item.id);
@ -101,7 +101,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
defaultMessage: 'Edit job'
}),
icon: 'pencil',
enabled: () => (canUpdateJob && canUpdateDatafeed),
enabled: (item) => (item.deleting !== true && canUpdateJob && canUpdateDatafeed),
onClick: (item) => {
showEditJobFlyout(item);
closeMenu();
@ -115,7 +115,7 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt
}),
icon: 'trash',
color: 'danger',
enabled: () => (canDeleteJob),
enabled: () => canDeleteJob,
onClick: (item) => {
showDeleteJobModal([item]);
closeMenu();

View file

@ -54,6 +54,7 @@ function ResultLinksUI({ jobs, intl }) {
});
const singleMetricVisible = (jobs.length < 2);
const singleMetricEnabled = (jobs.length === 1 && jobs[0].isSingleMetricViewerJob);
const jobActionsDisabled = (jobs.length === 1 && jobs[0].deleting === true);
return (
<React.Fragment>
{(singleMetricVisible) &&
@ -66,7 +67,7 @@ function ResultLinksUI({ jobs, intl }) {
iconType="stats"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
isDisabled={(singleMetricEnabled === false)}
isDisabled={(singleMetricEnabled === false || jobActionsDisabled === true)}
/>
</EuiToolTip>
}
@ -79,6 +80,7 @@ function ResultLinksUI({ jobs, intl }) {
iconType="tableOfContents"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
isDisabled={(jobActionsDisabled === true)}
/>
</EuiToolTip>
<div className="actions-border"/>

View file

@ -5,6 +5,8 @@
*/
import { stringHash } from '../../../../../common/util/string_utils';
import PropTypes from 'prop-types';
import React from 'react';
@ -53,17 +55,3 @@ function tabColor(name) {
return colorMap[name];
}
}
function stringHash(str) {
let hash = 0;
let chr = '';
if (str.length === 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash < 0 ? hash * -2 : hash;
}

View file

@ -101,7 +101,7 @@ class JobsListUI extends Component {
render() {
const { intl, loading } = this.props;
const selectionControls = {
selectable: () => true,
selectable: job => (job.deleting !== true),
selectableMessage: (selectable) => (!selectable) ? intl.formatMessage({
id: 'xpack.ml.jobsList.cannotSelectJobTooltip',
defaultMessage: 'Cannot select job' })
@ -115,6 +115,7 @@ class JobsListUI extends Component {
render: (item) => (
<EuiButtonIcon
onClick={() => this.toggleRow(item)}
isDisabled={(item.deleting === true)}
iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'}
aria-label={this.state.itemIdToExpandedRowMap[item.id]
? intl.formatMessage({

View file

@ -22,6 +22,7 @@ import { JobStatsBar } from '../jobs_stats_bar';
import { NodeAvailableWarning } from '../node_available_warning';
import { UpgradeWarning } from '../../../../components/upgrade';
import { RefreshJobsListButton } from '../refresh_jobs_list_button';
import { isEqual } from 'lodash';
import React, {
Component
@ -35,7 +36,9 @@ import {
const DEFAULT_REFRESH_INTERVAL_MS = 30000;
const MINIMUM_REFRESH_INTERVAL_MS = 5000;
const DELETING_JOBS_REFRESH_INTERVAL_MS = 2000;
let jobsRefreshInterval = null;
let deletingJobsRefreshTimeout = null;
export class JobsListView extends Component {
constructor(props) {
@ -50,6 +53,7 @@ export class JobsListView extends Component {
selectedJobs: [],
itemIdToExpandedRowMap: {},
filterClauses: [],
deletingJobIds: [],
};
this.updateFunctions = {};
@ -285,7 +289,19 @@ export class JobsListView extends Component {
this.updateFunctions[j].setState({ job: fullJobsList[j] });
});
jobs.forEach((job) => {
if (job.deleting && this.state.itemIdToExpandedRowMap[job.id]) {
this.toggleRow(job.id);
}
});
this.isDoneRefreshing();
if (jobsSummaryList.some(j => j.deleting === true)) {
// if there are some jobs in a deleting state, start polling for
// deleting jobs so we can update the jobs list once the
// deleting tasks are over
this.checkDeletingJobTasks();
}
} catch (error) {
console.error(error);
this.setState({ loading: false });
@ -293,6 +309,24 @@ export class JobsListView extends Component {
}
}
async checkDeletingJobTasks() {
const { jobIds } = await ml.jobs.deletingJobTasks();
if (jobIds.length === 0 || isEqual(jobIds.sort(), this.state.deletingJobIds.sort())) {
this.setState({
deletingJobIds: jobIds,
});
this.refreshJobSummaryList(true);
}
if (jobIds.length > 0 && deletingJobsRefreshTimeout === null) {
deletingJobsRefreshTimeout = setTimeout(() => {
deletingJobsRefreshTimeout = null;
this.checkDeletingJobTasks();
}, DELETING_JOBS_REFRESH_INTERVAL_MS);
}
}
renderJobsListComponents() {
const { loading, jobsSummaryList } = this.state;
const jobIds = jobsSummaryList.map(j => j.id);

View file

@ -100,4 +100,11 @@ export const jobs = {
});
},
deletingJobTasks() {
return http({
url: `${basePath}/jobs/deleting_jobs_tasks`,
method: 'GET',
});
},
};

View file

@ -83,15 +83,7 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
ml.closeJob = ca({
urls: [
{
fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close',
req: {
jobId: {
type: 'string'
}
}
},
{
fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close?force=true',
fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close?force=<%=force%>',
req: {
jobId: {
type: 'string'
@ -100,30 +92,41 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
type: 'boolean'
}
}
},
{
fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close',
req: {
jobId: {
type: 'string'
}
}
}
],
method: 'POST'
});
ml.deleteJob = ca({
urls: [{
fmt: '/_ml/anomaly_detectors/<%=jobId%>',
req: {
jobId: {
type: 'string'
urls: [
{
fmt: '/_ml/anomaly_detectors/<%=jobId%>?&force=<%=force%>&wait_for_completion=false',
req: {
jobId: {
type: 'string'
},
force: {
type: 'boolean'
}
}
},
{
fmt: '/_ml/anomaly_detectors/<%=jobId%>?&wait_for_completion=false',
req: {
jobId: {
type: 'string'
}
}
}
}, {
fmt: '/_ml/anomaly_detectors/<%=jobId%>?force=true',
req: {
jobId: {
type: 'string'
},
force: {
type: 'boolean'
}
}
}],
],
method: 'DELETE'
});
@ -207,24 +210,27 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
});
ml.deleteDatafeed = ca({
urls: [{
fmt: '/_ml/datafeeds/<%=datafeedId%>',
req: {
datafeedId: {
type: 'string'
urls: [
{
fmt: '/_ml/datafeeds/<%=datafeedId%>?force=<%=force%>',
req: {
datafeedId: {
type: 'string'
},
force: {
type: 'boolean'
}
}
},
{
fmt: '/_ml/datafeeds/<%=datafeedId%>',
req: {
datafeedId: {
type: 'string'
}
}
}
}, {
fmt: '/_ml/datafeeds/<%=datafeedId%>?force=true',
req: {
datafeedId: {
type: 'string'
},
force: {
type: 'boolean'
}
}
}],
],
method: 'DELETE'
});

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states';
import { datafeedsProvider } from './datafeeds';
import { jobAuditMessagesProvider } from '../job_audit_messages';
@ -95,8 +96,12 @@ export function jobsProvider(callWithRequest) {
return p;
}, {});
const deletingStr = i18n.translate('xpack.ml.models.jobService.deletingJob', {
defaultMessage: 'deleting',
});
const jobs = fullJobsList.map((job) => {
const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length);
const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0);
const {
earliest: earliestTimestampMs,
latest: latestTimestampMs } = earliestAndLatestTimestamps(job.data_counts);
@ -107,7 +112,7 @@ export function jobsProvider(callWithRequest) {
groups: (Array.isArray(job.groups) ? job.groups.sort() : []),
processed_record_count: job.data_counts.processed_record_count,
memory_status: (job.model_size_stats) ? job.model_size_stats.memory_status : '',
jobState: job.state,
jobState: (job.deleting === true) ? deletingStr : job.state,
hasDatafeed,
datafeedId: (hasDatafeed && job.datafeed_config.datafeed_id) ? job.datafeed_config.datafeed_id : '',
datafeedIndices: (hasDatafeed && job.datafeed_config.indices) ? job.datafeed_config.indices : [],
@ -116,6 +121,7 @@ export function jobsProvider(callWithRequest) {
earliestTimestampMs,
isSingleMetricViewerJob: isTimeSeriesViewJob(job),
nodeName: (job.node) ? job.node.name : undefined,
deleting: (job.deleting || undefined),
};
if (jobIds.find(j => (j === tempJob.id))) {
tempJob.fullJob = job;
@ -259,11 +265,33 @@ export function jobsProvider(callWithRequest) {
return obj;
}
async function deletingJobTasks() {
const actions = ['cluster:admin/xpack/ml/job/delete'];
const detailed = true;
const jobIds = [];
try {
const tasksList = await callWithRequest('tasks.list', { actions, detailed });
Object.keys(tasksList.nodes).forEach((nodeId) => {
const tasks = tasksList.nodes[nodeId].tasks;
Object.keys(tasks).forEach((taskId) => {
jobIds.push(tasks[taskId].description.replace(/^delete-job-/, ''));
});
});
} catch (e) {
// if the user doesn't have permission to load the task list,
// use the jobs list to get the ids of deleting jobs
const { jobs } = await callWithRequest('ml.jobs');
jobIds.push(...jobs.filter(j => j.deleting === true).map(j => j.job_id));
}
return { jobIds };
}
return {
forceDeleteJob,
deleteJobs,
closeJobs,
jobsSummary,
createFullJobsList,
deletingJobTasks,
};
}

View file

@ -134,4 +134,18 @@ export function jobServiceRoutes(server, commonRouteConfig) {
}
});
server.route({
method: 'GET',
path: '/api/ml/jobs/deleting_jobs_tasks',
handler(request) {
const callWithRequest = callWithRequestFactory(server, request);
const { deletingJobTasks } = jobServiceProvider(callWithRequest);
return deletingJobTasks()
.catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig
}
});
}