[upgrade] Add cancel button to reindexing (#29913)
This commit is contained in:
parent
20ffce229b
commit
d166001a88
16 changed files with 435 additions and 109 deletions
|
@ -27,6 +27,7 @@ export enum ReindexStatus {
|
|||
completed,
|
||||
failed,
|
||||
paused,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation';
|
||||
|
|
|
@ -94,6 +94,12 @@ export class ReindexButton extends React.Component<ReindexButtonProps, ReindexBu
|
|||
buttonProps.iconSide = 'left';
|
||||
buttonProps.iconType = 'pause';
|
||||
buttonContent = 'Paused';
|
||||
case ReindexStatus.cancelled:
|
||||
buttonProps.color = 'danger';
|
||||
buttonProps.iconSide = 'left';
|
||||
buttonProps.iconType = 'cross';
|
||||
buttonContent = 'Cancelled';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,6 +113,7 @@ export class ReindexButton extends React.Component<ReindexButtonProps, ReindexBu
|
|||
closeFlyout={this.closeFlyout}
|
||||
reindexState={reindexState}
|
||||
startReindex={this.startReindex}
|
||||
cancelReindex={this.cancelReindex}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
@ -144,6 +151,11 @@ export class ReindexButton extends React.Component<ReindexButtonProps, ReindexBu
|
|||
await this.service.startReindex();
|
||||
};
|
||||
|
||||
private cancelReindex = async () => {
|
||||
this.sendUIReindexTelemetryInfo('stop');
|
||||
await this.service.cancelReindex();
|
||||
};
|
||||
|
||||
private showFlyout = () => {
|
||||
this.sendUIReindexTelemetryInfo('open');
|
||||
this.setState({ flyoutVisible: true });
|
||||
|
|
|
@ -28,8 +28,20 @@ exports[`ChecklistFlyout renders 1`] = `
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<Component
|
||||
errorMessage={null}
|
||||
reindexTaskPercComplete={null}
|
||||
cancelReindex={[MockFunction]}
|
||||
reindexState={
|
||||
Object {
|
||||
"errorMessage": null,
|
||||
"hasRequiredPrivileges": true,
|
||||
"lastCompletedStep": undefined,
|
||||
"loadingState": 1,
|
||||
"reindexTaskPercComplete": null,
|
||||
"reindexWarnings": Array [
|
||||
0,
|
||||
],
|
||||
"status": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
|
|
@ -18,6 +18,7 @@ $stepStatusToCallOutColor: (
|
|||
failed: 'danger',
|
||||
complete: 'success',
|
||||
paused: 'warning',
|
||||
cancelled: 'danger',
|
||||
);
|
||||
|
||||
.upgStepProgress__status--circle {
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('ChecklistFlyout', () => {
|
|||
confirmInputValue: 'CONFIRM',
|
||||
onConfirmInputChange: jest.fn(),
|
||||
startReindex: jest.fn(),
|
||||
cancelReindex: jest.fn(),
|
||||
reindexState: {
|
||||
loadingState: LoadingState.Success,
|
||||
lastCompletedStep: undefined,
|
||||
|
|
|
@ -44,16 +44,9 @@ export const ChecklistFlyoutStep: React.StatelessComponent<{
|
|||
closeFlyout: () => void;
|
||||
reindexState: ReindexState;
|
||||
startReindex: () => void;
|
||||
}> = ({ closeFlyout, reindexState, startReindex }) => {
|
||||
const {
|
||||
loadingState,
|
||||
status,
|
||||
reindexTaskPercComplete,
|
||||
lastCompletedStep,
|
||||
errorMessage,
|
||||
hasRequiredPrivileges,
|
||||
indexGroup,
|
||||
} = reindexState;
|
||||
cancelReindex: () => void;
|
||||
}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => {
|
||||
const { loadingState, status, hasRequiredPrivileges } = reindexState;
|
||||
const loading = loadingState === LoadingState.Loading || status === ReindexStatus.inProgress;
|
||||
|
||||
return (
|
||||
|
@ -87,13 +80,7 @@ export const ChecklistFlyoutStep: React.StatelessComponent<{
|
|||
<EuiTitle size="xs">
|
||||
<h3>Reindexing process</h3>
|
||||
</EuiTitle>
|
||||
<ReindexProgress
|
||||
indexGroup={indexGroup}
|
||||
lastCompletedStep={lastCompletedStep}
|
||||
reindexStatus={status}
|
||||
reindexTaskPercComplete={reindexTaskPercComplete}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
<ReindexProgress reindexState={reindexState} cancelReindex={cancelReindex} />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
|
|
@ -22,6 +22,7 @@ interface ReindexFlyoutProps {
|
|||
closeFlyout: () => void;
|
||||
reindexState: ReindexState;
|
||||
startReindex: () => void;
|
||||
cancelReindex: () => void;
|
||||
}
|
||||
|
||||
interface ReindexFlyoutState {
|
||||
|
@ -46,7 +47,7 @@ export class ReindexFlyout extends React.Component<ReindexFlyoutProps, ReindexFl
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { closeFlyout, indexName, reindexState, startReindex } = this.props;
|
||||
const { closeFlyout, indexName, reindexState, startReindex, cancelReindex } = this.props;
|
||||
const { currentFlyoutStep } = this.state;
|
||||
|
||||
let flyoutContents: React.ReactNode;
|
||||
|
@ -66,6 +67,7 @@ export class ReindexFlyout extends React.Component<ReindexFlyoutProps, ReindexFl
|
|||
closeFlyout={closeFlyout}
|
||||
reindexState={reindexState}
|
||||
startReindex={startReindex}
|
||||
cancelReindex={cancelReindex}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -8,16 +8,22 @@ import { shallow } from 'enzyme';
|
|||
import React from 'react';
|
||||
|
||||
import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types';
|
||||
import { ReindexState } from '../polling_service';
|
||||
import { ReindexProgress } from './progress';
|
||||
|
||||
describe('ReindexProgress', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(
|
||||
<ReindexProgress
|
||||
lastCompletedStep={ReindexStep.created}
|
||||
reindexStatus={ReindexStatus.inProgress}
|
||||
reindexTaskPercComplete={null}
|
||||
errorMessage={null}
|
||||
reindexState={
|
||||
{
|
||||
lastCompletedStep: ReindexStep.created,
|
||||
status: ReindexStatus.inProgress,
|
||||
reindexTaskPercComplete: null,
|
||||
errorMessage: null,
|
||||
} as ReindexState
|
||||
}
|
||||
cancelReindex={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -50,10 +56,15 @@ describe('ReindexProgress', () => {
|
|||
it('displays errors in the step that failed', () => {
|
||||
const wrapper = shallow(
|
||||
<ReindexProgress
|
||||
lastCompletedStep={ReindexStep.reindexCompleted}
|
||||
reindexStatus={ReindexStatus.failed}
|
||||
reindexTaskPercComplete={1}
|
||||
errorMessage={`This is an error that happened on alias switch`}
|
||||
reindexState={
|
||||
{
|
||||
lastCompletedStep: ReindexStep.reindexCompleted,
|
||||
status: ReindexStatus.failed,
|
||||
reindexTaskPercComplete: 1,
|
||||
errorMessage: `This is an error that happened on alias switch`,
|
||||
} as ReindexState
|
||||
}
|
||||
cancelReindex={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -66,26 +77,36 @@ describe('ReindexProgress', () => {
|
|||
it('shows reindexing document progress bar', () => {
|
||||
const wrapper = shallow(
|
||||
<ReindexProgress
|
||||
lastCompletedStep={ReindexStep.reindexStarted}
|
||||
reindexStatus={ReindexStatus.inProgress}
|
||||
reindexTaskPercComplete={0.25}
|
||||
errorMessage={null}
|
||||
reindexState={
|
||||
{
|
||||
lastCompletedStep: ReindexStep.reindexStarted,
|
||||
status: ReindexStatus.inProgress,
|
||||
reindexTaskPercComplete: 0.25,
|
||||
errorMessage: null,
|
||||
} as ReindexState
|
||||
}
|
||||
cancelReindex={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const reindexStep = wrapper.props().steps[2];
|
||||
expect(reindexStep.children.type.name).toEqual('EuiProgress');
|
||||
expect(reindexStep.children.props.value).toEqual(0.25);
|
||||
expect(reindexStep.children.type.name).toEqual('ReindexProgressBar');
|
||||
expect(reindexStep.children.props.reindexState.reindexTaskPercComplete).toEqual(0.25);
|
||||
});
|
||||
|
||||
it('adds steps for index groups', () => {
|
||||
const wrapper = shallow(
|
||||
<ReindexProgress
|
||||
lastCompletedStep={ReindexStep.created}
|
||||
reindexStatus={ReindexStatus.inProgress}
|
||||
indexGroup={IndexGroup.ml}
|
||||
reindexTaskPercComplete={null}
|
||||
errorMessage={null}
|
||||
reindexState={
|
||||
{
|
||||
lastCompletedStep: ReindexStep.created,
|
||||
status: ReindexStatus.inProgress,
|
||||
indexGroup: IndexGroup.ml,
|
||||
reindexTaskPercComplete: null,
|
||||
errorMessage: null,
|
||||
} as ReindexState
|
||||
}
|
||||
cancelReindex={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,9 +6,18 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiCallOut, EuiProgress, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiProgress,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types';
|
||||
import { LoadingState } from '../../../../../types';
|
||||
import { ReindexState } from '../polling_service';
|
||||
import { StepProgress, StepProgressStep } from './step_progress';
|
||||
|
||||
const ErrorCallout: React.StatelessComponent<{ errorMessage: string | null }> = ({
|
||||
|
@ -28,6 +37,54 @@ const PausedCallout = () => (
|
|||
/>
|
||||
);
|
||||
|
||||
const ReindexProgressBar: React.StatelessComponent<{
|
||||
reindexState: ReindexState;
|
||||
cancelReindex: () => void;
|
||||
}> = ({
|
||||
reindexState: { lastCompletedStep, status, reindexTaskPercComplete, cancelLoadingState },
|
||||
cancelReindex,
|
||||
}) => {
|
||||
const progressBar = reindexTaskPercComplete ? (
|
||||
<EuiProgress size="s" value={reindexTaskPercComplete} max={1} />
|
||||
) : (
|
||||
<EuiProgress size="s" />
|
||||
);
|
||||
|
||||
let cancelText: string;
|
||||
switch (cancelLoadingState) {
|
||||
case LoadingState.Loading:
|
||||
cancelText = 'Cancelling…';
|
||||
break;
|
||||
case LoadingState.Success:
|
||||
cancelText = 'Cancelled';
|
||||
break;
|
||||
case LoadingState.Error:
|
||||
cancelText = 'Could not cancel';
|
||||
break;
|
||||
default:
|
||||
cancelText = 'Cancel';
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem>{progressBar}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={cancelReindex}
|
||||
disabled={
|
||||
cancelLoadingState === LoadingState.Loading ||
|
||||
status !== ReindexStatus.inProgress ||
|
||||
lastCompletedStep !== ReindexStep.reindexStarted
|
||||
}
|
||||
isLoading={cancelLoadingState === LoadingState.Loading}
|
||||
>
|
||||
{cancelText}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const orderedSteps = Object.values(ReindexStep).sort() as number[];
|
||||
|
||||
/**
|
||||
|
@ -35,32 +92,28 @@ const orderedSteps = Object.values(ReindexStep).sort() as number[];
|
|||
* and any error messages that are encountered.
|
||||
*/
|
||||
export const ReindexProgress: React.StatelessComponent<{
|
||||
lastCompletedStep?: ReindexStep;
|
||||
reindexStatus?: ReindexStatus;
|
||||
indexGroup?: IndexGroup;
|
||||
reindexTaskPercComplete: number | null;
|
||||
errorMessage: string | null;
|
||||
}> = ({
|
||||
lastCompletedStep = -1,
|
||||
reindexStatus,
|
||||
indexGroup,
|
||||
reindexTaskPercComplete,
|
||||
errorMessage,
|
||||
}) => {
|
||||
reindexState: ReindexState;
|
||||
cancelReindex: () => void;
|
||||
}> = props => {
|
||||
const { errorMessage, indexGroup, lastCompletedStep = -1, status } = props.reindexState;
|
||||
const stepDetails = (thisStep: ReindexStep): Pick<StepProgressStep, 'status' | 'children'> => {
|
||||
const previousStep = orderedSteps[orderedSteps.indexOf(thisStep) - 1];
|
||||
|
||||
if (reindexStatus === ReindexStatus.failed && lastCompletedStep === previousStep) {
|
||||
if (status === ReindexStatus.failed && lastCompletedStep === previousStep) {
|
||||
return {
|
||||
status: 'failed',
|
||||
children: <ErrorCallout {...{ errorMessage }} />,
|
||||
};
|
||||
} else if (reindexStatus === ReindexStatus.paused && lastCompletedStep === previousStep) {
|
||||
} else if (status === ReindexStatus.paused && lastCompletedStep === previousStep) {
|
||||
return {
|
||||
status: 'paused',
|
||||
children: <PausedCallout />,
|
||||
};
|
||||
} else if (reindexStatus === undefined || lastCompletedStep < previousStep) {
|
||||
} else if (status === ReindexStatus.cancelled && lastCompletedStep === previousStep) {
|
||||
return {
|
||||
status: 'cancelled',
|
||||
};
|
||||
} else if (status === undefined || lastCompletedStep < previousStep) {
|
||||
return {
|
||||
status: 'incomplete',
|
||||
};
|
||||
|
@ -79,33 +132,35 @@ export const ReindexProgress: React.StatelessComponent<{
|
|||
// with a progress bar.
|
||||
const reindexingDocsStep = { title: 'Reindexing documents' } as StepProgressStep;
|
||||
if (
|
||||
reindexStatus === ReindexStatus.failed &&
|
||||
status === ReindexStatus.failed &&
|
||||
(lastCompletedStep === ReindexStep.newIndexCreated ||
|
||||
lastCompletedStep === ReindexStep.reindexStarted)
|
||||
) {
|
||||
reindexingDocsStep.status = 'failed';
|
||||
reindexingDocsStep.children = <ErrorCallout {...{ errorMessage }} />;
|
||||
} else if (
|
||||
reindexStatus === ReindexStatus.paused &&
|
||||
status === ReindexStatus.paused &&
|
||||
(lastCompletedStep === ReindexStep.newIndexCreated ||
|
||||
lastCompletedStep === ReindexStep.reindexStarted)
|
||||
) {
|
||||
reindexingDocsStep.status = 'paused';
|
||||
reindexingDocsStep.children = <PausedCallout />;
|
||||
} else if (reindexStatus === undefined || lastCompletedStep < ReindexStep.newIndexCreated) {
|
||||
} else if (
|
||||
status === ReindexStatus.cancelled &&
|
||||
(lastCompletedStep === ReindexStep.newIndexCreated ||
|
||||
lastCompletedStep === ReindexStep.reindexStarted)
|
||||
) {
|
||||
reindexingDocsStep.status = 'cancelled';
|
||||
} else if (status === undefined || lastCompletedStep < ReindexStep.newIndexCreated) {
|
||||
reindexingDocsStep.status = 'incomplete';
|
||||
} else if (
|
||||
lastCompletedStep === ReindexStep.newIndexCreated ||
|
||||
lastCompletedStep === ReindexStep.reindexStarted
|
||||
) {
|
||||
reindexingDocsStep.status = 'inProgress';
|
||||
reindexingDocsStep.children = <ReindexProgressBar {...props} />;
|
||||
} else {
|
||||
reindexingDocsStep.status =
|
||||
lastCompletedStep === ReindexStep.newIndexCreated ||
|
||||
lastCompletedStep === ReindexStep.reindexStarted
|
||||
? 'inProgress'
|
||||
: 'complete';
|
||||
|
||||
reindexingDocsStep.children = reindexTaskPercComplete ? (
|
||||
<EuiProgress size="s" value={reindexTaskPercComplete} max={1} />
|
||||
) : (
|
||||
<EuiProgress size="s" />
|
||||
);
|
||||
reindexingDocsStep.status = 'complete';
|
||||
}
|
||||
|
||||
const steps = [
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { Fragment, ReactNode } from 'react';
|
|||
|
||||
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused';
|
||||
type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused' | 'cancelled';
|
||||
|
||||
const StepStatus: React.StatelessComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => {
|
||||
if (status === 'incomplete') {
|
||||
|
@ -28,6 +28,12 @@ const StepStatus: React.StatelessComponent<{ status: STATUS; idx: number }> = ({
|
|||
<EuiIcon type="pause" size="s" />
|
||||
</span>
|
||||
);
|
||||
} else if (status === 'cancelled') {
|
||||
return (
|
||||
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-cancelled">
|
||||
<EuiIcon type="cross" size="s" />
|
||||
</span>
|
||||
);
|
||||
} else if (status === 'failed') {
|
||||
return (
|
||||
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-failed">
|
||||
|
@ -47,7 +53,10 @@ const Step: React.StatelessComponent<StepProgressStep & { idx: number }> = ({
|
|||
}) => {
|
||||
const titleClassName = classNames('upgStepProgress__title', {
|
||||
'upgStepProgress__title--currentStep':
|
||||
status === 'inProgress' || status === 'paused' || status === 'failed',
|
||||
status === 'inProgress' ||
|
||||
status === 'paused' ||
|
||||
status === 'failed' ||
|
||||
status === 'cancelled',
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -98,4 +98,13 @@ describe('ReindexPollingService', () => {
|
|||
expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelReindex', () => {
|
||||
it('posts to cancel endpoint', async () => {
|
||||
const service = new ReindexPollingService('myIndex');
|
||||
await service.cancelReindex();
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex/cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ export const APIClient = axios.create({
|
|||
|
||||
export interface ReindexState {
|
||||
loadingState: LoadingState;
|
||||
cancelLoadingState?: LoadingState;
|
||||
lastCompletedStep?: ReindexStep;
|
||||
status?: ReindexStatus;
|
||||
reindexTaskPercComplete: number | null;
|
||||
|
@ -103,6 +104,7 @@ export class ReindexPollingService {
|
|||
status: ReindexStatus.inProgress,
|
||||
reindexTaskPercComplete: null,
|
||||
errorMessage: null,
|
||||
cancelLoadingState: undefined,
|
||||
});
|
||||
const { data } = await APIClient.post<ReindexOperation>(
|
||||
chrome.addBasePath(`/api/upgrade_assistant/reindex/${this.indexName}`)
|
||||
|
@ -115,16 +117,35 @@ export class ReindexPollingService {
|
|||
}
|
||||
};
|
||||
|
||||
public cancelReindex = async () => {
|
||||
try {
|
||||
this.status$.next({
|
||||
...this.status$.value,
|
||||
cancelLoadingState: LoadingState.Loading,
|
||||
});
|
||||
|
||||
await APIClient.post(
|
||||
chrome.addBasePath(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`)
|
||||
);
|
||||
} catch (e) {
|
||||
this.status$.next({
|
||||
...this.status$.value,
|
||||
cancelLoadingState: LoadingState.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private updateWithResponse = ({
|
||||
reindexOp,
|
||||
warnings,
|
||||
hasRequiredPrivileges,
|
||||
indexGroup,
|
||||
}: StatusResponse) => {
|
||||
const currentValue = this.status$.value;
|
||||
// Next value should always include the entire state, not just what changes.
|
||||
// We make a shallow copy as a starting new state.
|
||||
const nextValue = {
|
||||
...this.status$.value,
|
||||
...currentValue,
|
||||
// If we're getting any updates, set to success.
|
||||
loadingState: LoadingState.Success,
|
||||
};
|
||||
|
@ -142,10 +163,15 @@ export class ReindexPollingService {
|
|||
}
|
||||
|
||||
if (reindexOp) {
|
||||
// Prevent the UI flickering back to inProgres after cancelling.
|
||||
nextValue.lastCompletedStep = reindexOp.lastCompletedStep;
|
||||
nextValue.status = reindexOp.status;
|
||||
nextValue.reindexTaskPercComplete = reindexOp.reindexTaskPercComplete;
|
||||
nextValue.errorMessage = reindexOp.errorMessage;
|
||||
|
||||
if (reindexOp.status === ReindexStatus.cancelled) {
|
||||
nextValue.cancelLoadingState = LoadingState.Success;
|
||||
}
|
||||
}
|
||||
|
||||
this.status$.next(nextValue);
|
||||
|
|
|
@ -196,6 +196,22 @@ describe('reindexService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('deletes existing operation if it was cancelled', async () => {
|
||||
callCluster.mockResolvedValueOnce(true); // indices.exist
|
||||
actions.findReindexOperations.mockResolvedValueOnce({
|
||||
saved_objects: [{ id: 1, attributes: { status: ReindexStatus.cancelled } }],
|
||||
total: 1,
|
||||
});
|
||||
actions.deleteReindexOp.mockResolvedValueOnce();
|
||||
actions.createReindexOp.mockResolvedValueOnce();
|
||||
|
||||
await service.createReindexOperation('myIndex');
|
||||
expect(actions.deleteReindexOp).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
attributes: { status: ReindexStatus.cancelled },
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if existing operation did not fail', async () => {
|
||||
callCluster.mockResolvedValueOnce(true); // indices.exist
|
||||
actions.findReindexOperations.mockResolvedValueOnce({
|
||||
|
@ -284,7 +300,7 @@ describe('reindexService', () => {
|
|||
findSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws in reindex operation does not exist', async () => {
|
||||
it('throws if reindex operation does not exist', async () => {
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(null);
|
||||
await expect(service.pauseReindexOperation('myIndex')).rejects.toThrow();
|
||||
expect(actions.updateReindexOp).not.toHaveBeenCalled();
|
||||
|
@ -336,7 +352,7 @@ describe('reindexService', () => {
|
|||
findSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws in reindex operation does not exist', async () => {
|
||||
it('throws if reindex operation does not exist', async () => {
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(null);
|
||||
await expect(service.resumeReindexOperation('myIndex')).rejects.toThrow();
|
||||
expect(actions.updateReindexOp).not.toHaveBeenCalled();
|
||||
|
@ -344,6 +360,60 @@ describe('reindexService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cancelReindexing', () => {
|
||||
it('cancels the reindex task', async () => {
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce({
|
||||
id: '2',
|
||||
attributes: {
|
||||
indexName: 'myIndex',
|
||||
status: ReindexStatus.inProgress,
|
||||
lastCompletedStep: ReindexStep.reindexStarted,
|
||||
reindexTaskId: '999333',
|
||||
},
|
||||
});
|
||||
callCluster.mockResolvedValueOnce(true);
|
||||
|
||||
await service.cancelReindexing('myIndex');
|
||||
expect(callCluster).toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' });
|
||||
findSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws if reindexOp status is not inProgress', async () => {
|
||||
const reindexOp = {
|
||||
id: '2',
|
||||
attributes: { indexName: 'myIndex', status: ReindexStatus.failed, reindexTaskId: '999333' },
|
||||
} as ReindexSavedObject;
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(reindexOp);
|
||||
|
||||
await expect(service.cancelReindexing('myIndex')).rejects.toThrow();
|
||||
expect(callCluster).not.toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' });
|
||||
findSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws if reindexOp lastCompletedStep is not reindexStarted', async () => {
|
||||
const reindexOp = {
|
||||
id: '2',
|
||||
attributes: {
|
||||
indexName: 'myIndex',
|
||||
status: ReindexStatus.inProgress,
|
||||
lastCompletedStep: ReindexStep.reindexCompleted,
|
||||
reindexTaskId: '999333',
|
||||
},
|
||||
} as ReindexSavedObject;
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(reindexOp);
|
||||
|
||||
await expect(service.cancelReindexing('myIndex')).rejects.toThrow();
|
||||
expect(callCluster).not.toHaveBeenCalledWith('tasks.cancel', { taskId: '999333' });
|
||||
findSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws if reindex operation does not exist', async () => {
|
||||
const findSpy = jest.spyOn(service, 'findReindexOperation').mockResolvedValueOnce(null);
|
||||
await expect(service.cancelReindexing('myIndex')).rejects.toThrow();
|
||||
findSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('state machine, lastCompletedStep ===', () => {
|
||||
const defaultAttributes = {
|
||||
indexName: 'myIndex',
|
||||
|
@ -738,6 +808,26 @@ describe('reindexService', () => {
|
|||
expect(updatedOp.attributes.errorMessage).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reindex task is cancelled', () => {
|
||||
it('deletes tsk, updates status to cancelled', async () => {
|
||||
callCluster
|
||||
.mockResolvedValueOnce({
|
||||
completed: true,
|
||||
task: { status: { created: 100, total: 100, canceled: 'by user request' } },
|
||||
})
|
||||
.mockResolvedValue({ result: 'deleted' });
|
||||
|
||||
const updatedOp = await service.processNextStep(reindexOp);
|
||||
expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted);
|
||||
expect(updatedOp.attributes.status).toEqual(ReindexStatus.cancelled);
|
||||
expect(callCluster).toHaveBeenCalledWith('delete', {
|
||||
index: '.tasks',
|
||||
type: 'task',
|
||||
id: 'xyz',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reindexCompleted', () => {
|
||||
|
@ -827,7 +917,6 @@ describe('reindexService', () => {
|
|||
expect(updatedOp.attributes.lastCompletedStep).toEqual(
|
||||
ReindexStep.indexGroupServicesStarted
|
||||
);
|
||||
expect(updatedOp.attributes.status).toEqual(ReindexStatus.completed);
|
||||
expect(callCluster).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -929,7 +1018,6 @@ describe('reindexService', () => {
|
|||
expect(updatedOp.attributes.lastCompletedStep).toEqual(
|
||||
ReindexStep.indexGroupServicesStarted
|
||||
);
|
||||
expect(updatedOp.attributes.status).toEqual(ReindexStatus.completed);
|
||||
expect(callCluster).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -1020,5 +1108,20 @@ describe('reindexService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('indexGroupServicesStarted', () => {
|
||||
const reindexOp = {
|
||||
id: '1',
|
||||
attributes: {
|
||||
...defaultAttributes,
|
||||
lastCompletedStep: ReindexStep.indexGroupServicesStarted,
|
||||
},
|
||||
} as ReindexSavedObject;
|
||||
|
||||
it('sets to completed', async () => {
|
||||
const updatedOp = await service.processNextStep(reindexOp);
|
||||
expect(updatedOp.attributes.status).toEqual(ReindexStatus.completed);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,6 +77,15 @@ export interface ReindexService {
|
|||
* @param indexName
|
||||
*/
|
||||
resumeReindexOperation(indexName: string): Promise<ReindexSavedObject>;
|
||||
|
||||
/**
|
||||
* Cancel an in-progress reindex operation for a given index. Only allowed when the
|
||||
* reindex operation is in the ReindexStep.reindexStarted step. Relies on the ReindexWorker
|
||||
* to continue processing the reindex operation to detect that the Reindex Task in ES has been
|
||||
* cancelled.
|
||||
* @param indexName
|
||||
*/
|
||||
cancelReindexing(indexName: string): Promise<ReindexSavedObject>;
|
||||
}
|
||||
|
||||
export const reindexServiceFactory = (
|
||||
|
@ -171,7 +180,14 @@ export const reindexServiceFactory = (
|
|||
};
|
||||
|
||||
const cleanupChanges = async (reindexOp: ReindexSavedObject) => {
|
||||
// Set back to writable if we ever got past this point.
|
||||
// Cancel reindex task if it was started but not completed
|
||||
if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) {
|
||||
await callCluster('tasks.cancel', {
|
||||
taskId: reindexOp.attributes.reindexTaskId,
|
||||
}).catch(e => undefined); // Ignore any exceptions trying to cancel (it may have already completed).
|
||||
}
|
||||
|
||||
// Set index back to writable if we ever got past this point.
|
||||
if (reindexOp.attributes.lastCompletedStep >= ReindexStep.readonly) {
|
||||
await callCluster('indices.putSettings', {
|
||||
index: reindexOp.attributes.indexName,
|
||||
|
@ -179,10 +195,19 @@ export const reindexServiceFactory = (
|
|||
});
|
||||
}
|
||||
|
||||
// Stop consumers if we ever got past this point.
|
||||
if (
|
||||
reindexOp.attributes.lastCompletedStep >= ReindexStep.newIndexCreated &&
|
||||
reindexOp.attributes.lastCompletedStep < ReindexStep.aliasCreated
|
||||
) {
|
||||
await callCluster('indices.delete', { index: reindexOp.attributes.newIndexName });
|
||||
}
|
||||
|
||||
// Resume consumers if we ever got past this point.
|
||||
if (reindexOp.attributes.lastCompletedStep >= ReindexStep.indexGroupServicesStopped) {
|
||||
await resumeIndexGroupServices(reindexOp);
|
||||
}
|
||||
|
||||
return reindexOp;
|
||||
};
|
||||
|
||||
// ------ Functions used to process the state machine
|
||||
|
@ -306,8 +331,24 @@ export const reindexServiceFactory = (
|
|||
waitForCompletion: false,
|
||||
});
|
||||
|
||||
if (taskResponse.completed) {
|
||||
if (!taskResponse.completed) {
|
||||
// Updated the percent complete
|
||||
const perc = taskResponse.task.status.created / taskResponse.task.status.total;
|
||||
return actions.updateReindexOp(reindexOp, {
|
||||
reindexTaskPercComplete: perc,
|
||||
});
|
||||
} else if (taskResponse.task.status.canceled === 'by user request') {
|
||||
// Set the status to cancelled
|
||||
reindexOp = await actions.updateReindexOp(reindexOp, {
|
||||
status: ReindexStatus.cancelled,
|
||||
});
|
||||
|
||||
// Do any other cleanup work necessary
|
||||
reindexOp = await cleanupChanges(reindexOp);
|
||||
} else {
|
||||
// Check that it reindexed all documents
|
||||
const { count } = await callCluster('count', { index: reindexOp.attributes.indexName });
|
||||
|
||||
if (taskResponse.task.status.created < count) {
|
||||
if (taskResponse.response.failures && taskResponse.response.failures.length > 0) {
|
||||
const failureExample = JSON.stringify(taskResponse.response.failures[0]);
|
||||
|
@ -317,28 +358,25 @@ export const reindexServiceFactory = (
|
|||
}
|
||||
}
|
||||
|
||||
// Delete the task from ES .tasks index
|
||||
const deleteTaskResp = await callCluster('delete', {
|
||||
index: '.tasks',
|
||||
type: 'task',
|
||||
id: taskId,
|
||||
});
|
||||
|
||||
if (deleteTaskResp.result !== 'deleted') {
|
||||
throw Boom.badImplementation(`Could not delete reindexing task ${taskId}`);
|
||||
}
|
||||
|
||||
// Update the status
|
||||
return actions.updateReindexOp(reindexOp, {
|
||||
reindexOp = await actions.updateReindexOp(reindexOp, {
|
||||
lastCompletedStep: ReindexStep.reindexCompleted,
|
||||
reindexTaskPercComplete: 1,
|
||||
});
|
||||
} else {
|
||||
const perc = taskResponse.task.status.created / taskResponse.task.status.total;
|
||||
return actions.updateReindexOp(reindexOp, {
|
||||
reindexTaskPercComplete: perc,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the task from ES .tasks index
|
||||
const deleteTaskResp = await callCluster('delete', {
|
||||
index: '.tasks',
|
||||
type: 'task',
|
||||
id: taskId,
|
||||
});
|
||||
|
||||
if (deleteTaskResp.result !== 'deleted') {
|
||||
throw Boom.badImplementation(`Could not delete reindexing task ${taskId}`);
|
||||
}
|
||||
|
||||
return reindexOp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -382,11 +420,10 @@ export const reindexServiceFactory = (
|
|||
await startWatcher();
|
||||
}
|
||||
|
||||
// Only change the status if we're still in-progress (this function is also called when the reindex fails)
|
||||
// Only change the status if we're still in-progress (this function is also called when the reindex fails or is cancelled)
|
||||
if (reindexOp.attributes.status === ReindexStatus.inProgress) {
|
||||
return actions.updateReindexOp(reindexOp, {
|
||||
lastCompletedStep: ReindexStep.indexGroupServicesStarted,
|
||||
status: ReindexStatus.completed,
|
||||
});
|
||||
} else {
|
||||
return reindexOp;
|
||||
|
@ -461,8 +498,11 @@ export const reindexServiceFactory = (
|
|||
const existingReindexOps = await actions.findReindexOperations(indexName);
|
||||
if (existingReindexOps.total !== 0) {
|
||||
const existingOp = existingReindexOps.saved_objects[0];
|
||||
if (existingOp.attributes.status === ReindexStatus.failed) {
|
||||
// Delete the existing one if it failed to give a chance to retry.
|
||||
if (
|
||||
existingOp.attributes.status === ReindexStatus.failed ||
|
||||
existingOp.attributes.status === ReindexStatus.cancelled
|
||||
) {
|
||||
// Delete the existing one if it failed or was cancelled to give a chance to retry.
|
||||
await actions.deleteReindexOp(existingOp);
|
||||
} else {
|
||||
throw Boom.badImplementation(`A reindex operation already in-progress for ${indexName}`);
|
||||
|
@ -512,6 +552,10 @@ export const reindexServiceFactory = (
|
|||
case ReindexStep.aliasCreated:
|
||||
lockedReindexOp = await resumeIndexGroupServices(lockedReindexOp);
|
||||
break;
|
||||
case ReindexStep.indexGroupServicesStarted:
|
||||
lockedReindexOp = await actions.updateReindexOp(lockedReindexOp, {
|
||||
status: ReindexStatus.completed,
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -523,7 +567,7 @@ export const reindexServiceFactory = (
|
|||
});
|
||||
|
||||
// Cleanup any changes, ignoring any errors.
|
||||
await cleanupChanges(lockedReindexOp).catch(e => undefined);
|
||||
lockedReindexOp = await cleanupChanges(lockedReindexOp).catch(e => lockedReindexOp);
|
||||
}
|
||||
|
||||
return lockedReindexOp;
|
||||
|
@ -567,6 +611,28 @@ export const reindexServiceFactory = (
|
|||
return actions.updateReindexOp(op, { status: ReindexStatus.inProgress });
|
||||
});
|
||||
},
|
||||
|
||||
async cancelReindexing(indexName: string) {
|
||||
const reindexOp = await this.findReindexOperation(indexName);
|
||||
|
||||
if (!reindexOp) {
|
||||
throw new Error(`No reindex operation found for index ${indexName}`);
|
||||
} else if (reindexOp.attributes.status !== ReindexStatus.inProgress) {
|
||||
throw new Error(`Reindex operation is not in progress`);
|
||||
} else if (reindexOp.attributes.lastCompletedStep !== ReindexStep.reindexStarted) {
|
||||
throw new Error(`Reindex operation is not current waiting for reindex task to complete`);
|
||||
}
|
||||
|
||||
const resp = await callCluster('tasks.cancel', {
|
||||
taskId: reindexOp.attributes.reindexTaskId,
|
||||
});
|
||||
|
||||
if (resp.node_failures && resp.node_failures.length > 0) {
|
||||
throw new Error(`Could not cancel reindex.`);
|
||||
}
|
||||
|
||||
return reindexOp;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const mockReindexService = {
|
|||
findReindexOperation: jest.fn(),
|
||||
processNextStep: jest.fn(),
|
||||
resumeReindexOperation: jest.fn(),
|
||||
cancelReindexing: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../lib/reindexing', () => {
|
||||
|
@ -63,6 +64,7 @@ describe('reindex API', () => {
|
|||
mockReindexService.findReindexOperation.mockReset();
|
||||
mockReindexService.processNextStep.mockReset();
|
||||
mockReindexService.resumeReindexOperation.mockReset();
|
||||
mockReindexService.cancelReindexing.mockReset();
|
||||
worker.includes.mockReset();
|
||||
worker.forceRefresh.mockReset();
|
||||
|
||||
|
@ -209,14 +211,18 @@ describe('reindex API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/upgrade_assistant/reindex/{indexName}', () => {
|
||||
describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => {
|
||||
it('returns a 501', async () => {
|
||||
mockReindexService.cancelReindexing.mockResolvedValueOnce({});
|
||||
|
||||
const resp = await server.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/upgrade_assistant/reindex/cancelMe',
|
||||
method: 'POST',
|
||||
url: '/api/upgrade_assistant/reindex/cancelMe/cancel',
|
||||
});
|
||||
|
||||
expect(resp.statusCode).toEqual(501);
|
||||
expect(resp.statusCode).toEqual(200);
|
||||
expect(resp.payload).toMatchInlineSnapshot(`"{\\"acknowledged\\":true}"`);
|
||||
expect(mockReindexService.cancelReindexing).toHaveBeenCalledWith('cancelMe');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,7 +68,6 @@ export function registerReindexIndicesRoutes(
|
|||
async handler(request) {
|
||||
const client = request.getSavedObjectsClient();
|
||||
const { indexName } = request.params;
|
||||
|
||||
const callCluster = callWithRequest.bind(null, request) as CallCluster;
|
||||
const reindexActions = reindexActionsFactory(client, callCluster);
|
||||
const reindexService = reindexServiceFactory(callCluster, xpackInfo, reindexActions);
|
||||
|
@ -141,10 +140,26 @@ export function registerReindexIndicesRoutes(
|
|||
|
||||
// Cancel reindex
|
||||
server.route({
|
||||
path: `${BASE_PATH}/{indexName}`,
|
||||
method: 'DELETE',
|
||||
path: `${BASE_PATH}/{indexName}/cancel`,
|
||||
method: 'POST',
|
||||
async handler(request) {
|
||||
return Boom.notImplemented();
|
||||
const client = request.getSavedObjectsClient();
|
||||
const { indexName } = request.params;
|
||||
const callCluster = callWithRequest.bind(null, request) as CallCluster;
|
||||
const reindexActions = reindexActionsFactory(client, callCluster);
|
||||
const reindexService = reindexServiceFactory(callCluster, xpackInfo, reindexActions);
|
||||
|
||||
try {
|
||||
await reindexService.cancelReindexing(indexName);
|
||||
|
||||
return { acknowledged: true };
|
||||
} catch (e) {
|
||||
if (!e.isBoom) {
|
||||
return Boom.boomify(e, { statusCode: 500 });
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue