[upgrade] Add cancel button to reindexing (#29913)

This commit is contained in:
Josh Dover 2019-02-05 00:19:13 -06:00 committed by GitHub
parent 20ffce229b
commit d166001a88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 435 additions and 109 deletions

View file

@ -27,6 +27,7 @@ export enum ReindexStatus {
completed,
failed,
paused,
cancelled,
}
export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation';

View file

@ -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 });

View file

@ -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>

View file

@ -18,6 +18,7 @@ $stepStatusToCallOutColor: (
failed: 'danger',
complete: 'success',
paused: 'warning',
cancelled: 'danger',
);
.upgStepProgress__status--circle {

View file

@ -20,6 +20,7 @@ describe('ChecklistFlyout', () => {
confirmInputValue: 'CONFIRM',
onConfirmInputChange: jest.fn(),
startReindex: jest.fn(),
cancelReindex: jest.fn(),
reindexState: {
loadingState: LoadingState.Success,
lastCompletedStep: undefined,

View file

@ -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">

View file

@ -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;

View file

@ -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()}
/>
);

View file

@ -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 = [

View file

@ -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 (

View file

@ -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');
});
});
});

View file

@ -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);

View file

@ -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);
});
});
});
});

View file

@ -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;
},
};
};

View file

@ -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');
});
});
});

View file

@ -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;
}
},
});
}