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

Remove tasks with cleanup logic instead of marking them as failed #152841

Merged
merged 29 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b95bccd
Remove tasks instead of marking them as failed
mikecote Feb 22, 2023
b991e07
Merge branch 'main' of github.com:elastic/kibana into task-manager/re…
mikecote Mar 6, 2023
b583f34
Merge branch 'main' of github.com:elastic/kibana into task-manager/re…
mikecote Mar 7, 2023
9840419
Initial commit
mikecote Mar 7, 2023
4de2462
Merge with main
mikecote Mar 8, 2023
934674a
Add cleanup hook and handle tasks that are timed out and out of attempts
mikecote Mar 10, 2023
63a24fd
Merge with main
mikecote Mar 10, 2023
2c58dc3
Provide namespace to repository when deleting action_task_params
mikecote Mar 10, 2023
7448a6f
Merge branch 'main' of github.com:elastic/kibana into task-manager/ha…
mikecote Mar 20, 2023
b530399
Fix failing tests and cleanup bulkRemoveIfExists
mikecote Mar 20, 2023
0524509
Fix failing test
mikecote Mar 20, 2023
9ac4f15
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Mar 20, 2023
0e19d9a
Fix typechecks
mikecote Mar 21, 2023
4fbf8bd
Merge branch 'task-manager/handle-failed-tasks' of github.com:mikecot…
mikecote Mar 21, 2023
4e296e9
Added some unit tests
mikecote Mar 21, 2023
d3c5884
Merge branch 'main' of github.com:elastic/kibana into task-manager/ha…
mikecote Mar 21, 2023
f4965f4
Try different archives for rules and tasks
mikecote Mar 22, 2023
80b8e4f
Remove exclusive test
mikecote Mar 22, 2023
b112705
Merge branch 'main' into task-manager/handle-failed-tasks
kibanamachine Mar 22, 2023
3e7a773
Merge branch 'main' into task-manager/handle-failed-tasks
kibanamachine Mar 22, 2023
77fd8f7
Merge branch 'main' into task-manager/handle-failed-tasks
kibanamachine Mar 23, 2023
b0b3d68
Use two different data archives
mikecote Mar 23, 2023
704892e
Keep logged errors
mikecote Mar 23, 2023
82376d6
Add logger assertions back into unit tests
mikecote Mar 23, 2023
78846e4
Speed up polling cycle
mikecote Mar 23, 2023
d3658df
Merge branch 'main' into task-manager/handle-failed-tasks
kibanamachine Mar 24, 2023
c7b4e7c
Revert "Use two different data archives"
mikecote Mar 24, 2023
74c2a70
Add comments
mikecote Mar 24, 2023
07aed82
Merge branch 'task-manager/handle-failed-tasks' of github.com:mikecot…
mikecote Mar 24, 2023
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
3 changes: 1 addition & 2 deletions x-pack/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ export class ActionTypeRegistry {
[`actions:${actionType.id}`]: {
title: actionType.name,
maxAttempts,
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create(context, maxAttempts),
createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context),
},
});
// No need to notify usage on basic action types
Expand Down
157 changes: 38 additions & 119 deletions x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ import { TaskRunnerFactory } from './task_runner_factory';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { actionExecutorMock } from './action_executor.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { savedObjectsClientMock, loggingSystemMock, httpServiceMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
httpServiceMock,
savedObjectsRepositoryMock,
} from '@kbn/core/server/mocks';
import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks';
import { ActionTypeDisabledError } from './errors';
import { actionsClientMock } from '../mocks';
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
import { IN_MEMORY_METRICS } from '../monitoring';
import { pick } from 'lodash';
import { isRetryableError } from '@kbn/task-manager-plugin/server/task_running';
import {
isRetryableError,
isUnrecoverableError,
} from '@kbn/task-manager-plugin/server/task_running';

const executeParamsFields = [
'actionId',
Expand Down Expand Up @@ -86,15 +94,12 @@ const taskRunnerFactoryInitializerParams = {
logger: loggingSystemMock.create().get(),
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
basePathService: httpServiceMock.createBasePath(),
getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient),
savedObjectsRepository: savedObjectsRepositoryMock.create(),
};

beforeEach(() => {
jest.resetAllMocks();
actionExecutorInitializerParams.getServices.mockReturnValue(services);
taskRunnerFactoryInitializerParams.getUnsecuredSavedObjectsClient.mockReturnValue(
services.savedObjectsClient
);
});

test(`throws an error if factory isn't initialized`, () => {
Expand Down Expand Up @@ -410,36 +415,18 @@ test('executes the task by calling the executor with proper parameters when noti
);
});

test('cleans up action_task_params object', async () => {
test('cleans up action_task_params object through the cleanup runner method', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});

mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
type: 'action_task_params',
attributes: {
actionId: '2',
params: { baz: true },
executionId: '123abc',
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});

await taskRunner.run();
await taskRunner.cleanup();

expect(services.savedObjectsClient.delete).toHaveBeenCalledWith('action_task_params', '3', {
refresh: false,
});
expect(taskRunnerFactoryInitializerParams.savedObjectsRepository.delete).toHaveBeenCalledWith(
'action_task_params',
'3',
{ refresh: false }
);
});

test('task runner should implement CancellableTask cancel method with logging warning message', async () => {
Expand Down Expand Up @@ -474,37 +461,22 @@ test('task runner should implement CancellableTask cancel method with logging wa
);
});

test('runs successfully when cleanup fails and logs the error', async () => {
test('cleanup runs successfully when action_task_params cleanup fails and logs the error', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});

mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
type: 'action_task_params',
attributes: {
actionId: '2',
params: { baz: true },
executionId: '123abc',
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
services.savedObjectsClient.delete.mockRejectedValueOnce(new Error('Fail'));
taskRunnerFactoryInitializerParams.savedObjectsRepository.delete.mockRejectedValueOnce(
new Error('Fail')
);

await taskRunner.run();
await taskRunner.cleanup();

expect(services.savedObjectsClient.delete).toHaveBeenCalledWith('action_task_params', '3', {
refresh: false,
});
expect(taskRunnerFactoryInitializerParams.savedObjectsRepository.delete).toHaveBeenCalledWith(
'action_task_params',
'3',
{ refresh: false }
);
expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith(
'Failed to cleanup action_task_params object [id="3"]: Fail'
);
Expand Down Expand Up @@ -814,15 +786,12 @@ test(`doesn't use API key when not provided`, async () => {
});

test(`throws an error when license doesn't support the action type`, async () => {
const taskRunner = taskRunnerFactory.create(
{
taskInstance: {
...mockedTaskInstance,
attempts: 1,
},
const taskRunner = taskRunnerFactory.create({
taskInstance: {
...mockedTaskInstance,
attempts: 1,
},
2
);
});

mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
Expand All @@ -849,7 +818,7 @@ test(`throws an error when license doesn't support the action type`, async () =>
await taskRunner.run();
throw new Error('Should have thrown');
} catch (e) {
expect(isRetryableError(e)).toEqual(true);
expect(isUnrecoverableError(e)).toEqual(true);
}
});

Expand Down Expand Up @@ -894,57 +863,9 @@ test(`will throw an error with retry: false if the task is not retryable`, async
}
expect(err).toBeDefined();
expect(isRetryableError(err)).toEqual(false);
expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith(
`Action '2' failed and will not retry: Error message`
);
});

test(`treats errors as successes if the task is not retryable`, async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: {
...mockedTaskInstance,
attempts: 1,
},
});

mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
type: 'action_task_params',
attributes: {
actionId: '2',
params: { baz: true },
executionId: '123abc',
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
mockedActionExecutor.execute.mockResolvedValueOnce({
status: 'error',
actionId: '2',
message: 'Error message',
data: { foo: true },
retry: false,
});

let err;
try {
await taskRunner.run();
} catch (e) {
err = e;
}
expect(err).toBeUndefined();
expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith(
`Action '2' failed and will not retry: Error message`
);
});

test('will throw a retry error if the error is thrown instead of returned', async () => {
test('will rethrow the error if the error is thrown instead of returned', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: {
...mockedTaskInstance,
Expand All @@ -969,7 +890,8 @@ test('will throw a retry error if the error is thrown instead of returned', asyn
},
],
});
mockedActionExecutor.execute.mockRejectedValueOnce({});
const thrownError = new Error('Fail');
mockedActionExecutor.execute.mockRejectedValueOnce(thrownError);

let err;
try {
Expand All @@ -978,10 +900,7 @@ test('will throw a retry error if the error is thrown instead of returned', asyn
err = e;
}
expect(err).toBeDefined();
expect(isRetryableError(err)).toEqual(true);
expect(taskRunnerFactoryInitializerParams.logger.error as jest.Mock).toHaveBeenCalledWith(
`Action '2' failed and will retry: undefined`
);
expect(thrownError).toEqual(err);
});

test('increments monitoring metrics after execution', async () => {
Expand Down
Loading