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

[7.x] Implement wanted error handling for alerting and actions (#41917) #42255

Merged
merged 2 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(false);
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(false);
expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false);
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 ? false : error.retry;
}
// Don't retry other kinds of errors
return false;
},
createTaskRunner: this.taskRunCreatorFunction,
},
});
Expand Down
2 changes: 2 additions & 0 deletions x-pack/legacy/plugins/actions/server/lib/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ test('throws an error when config is invalid', async () => {
const result = await execute(executeParams);
expect(result).toEqual({
status: 'error',
retry: false,
message: `The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]`,
});
});
Expand Down Expand Up @@ -163,6 +164,7 @@ test('throws an error when params is invalid', async () => {
const result = await execute(executeParams);
expect(result).toEqual({
status: 'error',
retry: false,
message: `The actionParams is invalid: [param1]: expected value of type [string] but got [undefined]`,
});
});
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/actions/server/lib/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function execute({
validatedConfig = validateActionTypeConfig(actionType, mergedActionTypeConfig);
validatedParams = validateActionTypeParams(actionType, params);
} catch (err) {
return { status: 'error', message: err.message };
return { status: 'error', message: err.message, retry: false };
}

let result: ActionTypeExecutorResult | null = null;
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