Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement wanted error handling for alerting and actions #41917

Merged
merged 13 commits into from
Jul 30, 2019
58 changes: 41 additions & 17 deletions x-pack/legacy/plugins/actions/server/action_type_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/
import { ActionTypeRegistry } from './action_type_registry';
import { ExecutorType } from './types';
import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { ExecutorError } from './lib';

const mockTaskManager = taskManagerMock.create();

Expand Down Expand Up @@ -50,16 +51,18 @@ describe('register()', () => {
expect(actionTypeRegistry.has('my-action-type')).toEqual(true);
expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
expect(mockTaskManager.registerTaskDefinitions.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"actions:my-action-type": Object {
"createTaskRunner": [MockFunction],
"title": "My action type",
"type": "actions:my-action-type",
},
},
]
`);
Array [
Object {
"actions:my-action-type": Object {
"createTaskRunner": [MockFunction],
"getRetry": [Function],
"maxAttempts": 1,
"title": "My action type",
"type": "actions:my-action-type",
},
},
]
`);
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
expect(call.actionTypeRegistry).toBeTruthy();
Expand All @@ -86,6 +89,27 @@ Array [
`"Action type \\"my-action-type\\" is already registered."`
);
});

test('provides a getRetry function that handles ExecutorError', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: 'my-action-type',
name: 'My action type',
unencryptedAttributes: [],
executor,
});
expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
const registerTaskDefinitionsCall = mockTaskManager.registerTaskDefinitions.mock.calls[0][0];
const getRetry = registerTaskDefinitionsCall['actions:my-action-type'].getRetry!;

const retryTime = new Date();
expect(getRetry(0, new Error())).toEqual(true);
expect(getRetry(0, new ExecutorError('my message', {}, true))).toEqual(true);
expect(getRetry(0, new ExecutorError('my message', {}, false))).toEqual(false);
expect(getRetry(0, new ExecutorError('my message', {}, null))).toEqual(true);
expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(true);
expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime);
});
});

describe('get()', () => {
Expand All @@ -99,13 +123,13 @@ describe('get()', () => {
});
const actionType = actionTypeRegistry.get('my-action-type');
expect(actionType).toMatchInlineSnapshot(`
Object {
"executor": [Function],
"id": "my-action-type",
"name": "My action type",
"unencryptedAttributes": Array [],
}
`);
Object {
"executor": [Function],
"id": "my-action-type",
"name": "My action type",
"unencryptedAttributes": Array [],
}
`);
});

test(`throws an error when action type doesn't exist`, () => {
Expand Down
10 changes: 9 additions & 1 deletion x-pack/legacy/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { ActionType, GetServicesFunction } from './types';
import { TaskManager, TaskRunCreatorFunction } from '../../task_manager';
import { getCreateTaskRunnerFunction } from './lib';
import { getCreateTaskRunnerFunction, ExecutorError } from './lib';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';

interface ConstructorOptions {
Expand Down Expand Up @@ -61,6 +61,14 @@ export class ActionTypeRegistry {
[`actions:${actionType.id}`]: {
title: actionType.name,
type: `actions:${actionType.id}`,
maxAttempts: actionType.maxAttempts || 1,
getRetry(attempts: number, error: any) {
if (error instanceof ExecutorError) {
return error.retry == null ? true : error.retry;
mikecote marked this conversation as resolved.
Show resolved Hide resolved
}
// Retry other kinds of errors
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to have this return false by default. Idea being that an action will know when something is retry-able - setting retry to true or a Date - and any other case, should not be retryable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess by looking here https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/server/lib/execute.ts#L29-L74 any code that could throw here is already being handled and wrapped into a proper ExecutionError. Some may need to return retry: false ex validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and made it false for other kinds of errors. I can't think of a reason to retry totally unhandled errors.

},
createTaskRunner: this.taskRunCreatorFunction,
},
});
Expand Down
15 changes: 15 additions & 0 deletions x-pack/legacy/plugins/actions/server/lib/executor_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export class ExecutorError extends Error {
readonly data?: any;
readonly retry?: null | boolean | Date;
constructor(message?: string, data?: any, retry?: null | boolean | Date) {
super(message);
this.data = data;
this.retry = retry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { ExecutorError } from './executor_error';

const actionTypeRegistry = actionTypeRegistryMock.create();
const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
Expand Down Expand Up @@ -54,6 +55,9 @@ test('executes the task by calling the executor with proper parameters', async (
const { execute: mockExecute } = jest.requireMock('./execute');
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const runner = createTaskRunner({ taskInstance: taskInstanceMock });

mockExecute.mockResolvedValueOnce({ status: 'ok' });

const runnerResult = await runner.run();

expect(runnerResult).toBeUndefined();
Expand All @@ -66,3 +70,25 @@ test('executes the task by calling the executor with proper parameters', async (
params: { baz: true },
});
});

test('throws an error with suggested retry logic when return status is error', async () => {
const { execute: mockExecute } = jest.requireMock('./execute');
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const runner = createTaskRunner({ taskInstance: taskInstanceMock });

mockExecute.mockResolvedValueOnce({
status: 'error',
message: 'Error message',
data: { foo: true },
retry: false,
});

try {
await runner.run();
throw new Error('Should have thrown');
} catch (e) {
expect(e instanceof ExecutorError).toEqual(true);
expect(e.data).toEqual({ foo: true });
expect(e.retry).toEqual(false);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { execute } from './execute';
import { ExecutorError } from './executor_error';
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
import { TaskInstance } from '../../../task_manager';
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
Expand All @@ -28,14 +29,23 @@ export function getCreateTaskRunnerFunction({
return {
run: async () => {
const { namespace, id, actionTypeParams } = taskInstance.params;
await execute({
const executorResult = await execute({
namespace,
actionTypeRegistry,
encryptedSavedObjectsPlugin,
actionId: id,
services: getServices(taskInstance.params.basePath),
params: actionTypeParams,
});
if (executorResult.status === 'error') {
// Task manager error handler only kicks in when an error thrown (at this time)
// So what we have to do is throw when the return status is `error`.
throw new ExecutorError(
executorResult.message,
executorResult.data,
executorResult.retry
);
}
},
};
};
Expand Down
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/actions/server/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { execute } from './execute';
export { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
export { validateActionTypeConfig } from './validate_action_type_config';
export { validateActionTypeParams } from './validate_action_type_params';
export { ExecutorError } from './executor_error';
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/actions/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface ActionType {
id: string;
name: string;
unencryptedAttributes: string[];
maxAttempts?: number;
validate?: {
params?: { validate: (object: any) => any };
config?: { validate: (object: any) => any };
Expand Down
12 changes: 6 additions & 6 deletions x-pack/legacy/plugins/alerting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ This is the primary function for an alert type. Whenever the alert needs to exec
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.|
|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>**NOTE**: This currently only works when security is disabled. A future PR will add support for enabled security using Elasticsearch API tokens.|
|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)|
|scheduledRunAt|The date and time the alert type execution was scheduled to be called.|
|previousScheduledRunAt|The previous date and time the alert type was scheduled to be called.|
|startedAt|The date and time the alert type started execution.|
|previousStartedAt|The previous date and time the alert type started a successful execution.|
|params|Parameters for the execution. This is where the parameters you require will be passed in. (example threshold). Use alert type validation to ensure values are set before execution.|
|state|State returned from previous execution. This is the alert level state. What the executor returns will be serialized and provided here at the next execution.|

Expand All @@ -74,8 +74,8 @@ server.plugins.alerting.registerType({
}),
},
async executor({
scheduledRunAt,
previousScheduledRunAt,
startedAt,
previousStartedAt,
services,
params,
state,
Expand Down Expand Up @@ -131,8 +131,8 @@ server.plugins.alerting.registerType({
}),
},
async executor({
scheduledRunAt,
previousScheduledRunAt,
startedAt,
previousStartedAt,
services,
params,
state,
Expand Down
Loading