mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ResponseOps][Actions] Make actions retry when encountering failures (#143224)
* Updating maxAttempts to be 3 * Setting attempts to 3 in one place * Fixing test failure * Setting retry to true * Fixing tests * Fixing tests * Updating a missed test * Updating test instead of deleting * Adding constant
This commit is contained in:
parent
e42fe559c1
commit
b76caca9ef
15 changed files with 102 additions and 32 deletions
|
@ -70,7 +70,7 @@ describe('register()', () => {
|
|||
"actions:my-action-type": Object {
|
||||
"createTaskRunner": [Function],
|
||||
"getRetry": [Function],
|
||||
"maxAttempts": 1,
|
||||
"maxAttempts": 3,
|
||||
"title": "My action type",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,6 +25,8 @@ import {
|
|||
ActionTypeParams,
|
||||
} from './types';
|
||||
|
||||
export const MAX_ATTEMPTS: number = 3;
|
||||
|
||||
export interface ActionTypeRegistryOpts {
|
||||
licensing: LicensingPluginSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
|
@ -151,7 +153,7 @@ export class ActionTypeRegistry {
|
|||
this.taskManager.registerTaskDefinitions({
|
||||
[`actions:${actionType.id}`]: {
|
||||
title: actionType.name,
|
||||
maxAttempts: actionType.maxAttempts || 1,
|
||||
maxAttempts: actionType.maxAttempts || MAX_ATTEMPTS,
|
||||
getRetry(attempts: number, error: unknown) {
|
||||
if (error instanceof ExecutorError) {
|
||||
return error.retry == null ? false : error.retry;
|
||||
|
|
|
@ -540,7 +540,7 @@ test('logs a warning and error when alert executor throws an error', async () =>
|
|||
executorMock.mockRejectedValue(err);
|
||||
await actionExecutor.execute(executeParams);
|
||||
expect(loggerMock.warn).toBeCalledWith(
|
||||
'action execution failure: test:1: action-1: an error occurred while running the action: this action execution is intended to fail'
|
||||
'action execution failure: test:1: action-1: an error occurred while running the action: this action execution is intended to fail; retry: true'
|
||||
);
|
||||
expect(loggerMock.error).toBeCalledWith(err, {
|
||||
error: { stack_trace: 'foo error\n stack 1\n stack 2\n stack 3' },
|
||||
|
|
|
@ -242,7 +242,7 @@ export class ActionExecutor {
|
|||
message: 'an error occurred while running the action',
|
||||
serviceMessage: err.message,
|
||||
error: err,
|
||||
retry: false,
|
||||
retry: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
import { ActionsConfig, getValidatedConfig } from './config';
|
||||
import { resolveCustomHosts } from './lib/custom_host_settings';
|
||||
import { ActionsClient } from './actions_client';
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import { ActionTypeRegistry, MAX_ATTEMPTS } from './action_type_registry';
|
||||
import {
|
||||
createExecutionEnqueuerFunction,
|
||||
createEphemeralExecutionEnqueuerFunction,
|
||||
|
@ -351,6 +351,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
actionType: ActionType<Config, Secrets, Params, ExecutorResultData>
|
||||
) => {
|
||||
ensureSufficientLicense(actionType);
|
||||
actionType.maxAttempts = actionType.maxAttempts ?? MAX_ATTEMPTS;
|
||||
actionTypeRegistry.register(actionType);
|
||||
},
|
||||
registerSubActionConnectorType: <
|
||||
|
|
|
@ -223,6 +223,20 @@ async function executor(
|
|||
};
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
const message = i18n.translate(
|
||||
'xpack.stackConnectors.pagerduty.unexpectedNullResponseErrorMessage',
|
||||
{
|
||||
defaultMessage: 'unexpected null response from pagerduty',
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug(`response posting pagerduty event: ${response.status}`);
|
||||
|
||||
if (response.status === 202) {
|
||||
|
|
|
@ -136,6 +136,10 @@ async function teamsExecutor(
|
|||
})
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return errorResultUnexpectedNullResponse(actionId);
|
||||
}
|
||||
|
||||
if (isOk(result)) {
|
||||
const {
|
||||
value: { status, statusText, data: responseData, headers: responseHeaders },
|
||||
|
@ -206,6 +210,17 @@ function errorResultInvalid(
|
|||
};
|
||||
}
|
||||
|
||||
function errorResultUnexpectedNullResponse(actionId: string): ConnectorTypeExecutorResult<void> {
|
||||
const message = i18n.translate('xpack.stackConnectors.teams.unexpectedNullResponseErrorMessage', {
|
||||
defaultMessage: 'unexpected null response from Microsoft Teams',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function retryResult(actionId: string, message: string): ConnectorTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.stackConnectors.teams.errorPostingRetryLaterErrorMessage',
|
||||
|
|
|
@ -184,6 +184,10 @@ export async function executor(
|
|||
})
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return errorResultUnexpectedNullResponse(actionId);
|
||||
}
|
||||
|
||||
if (isOk(result)) {
|
||||
const {
|
||||
value: { status, statusText },
|
||||
|
@ -280,6 +284,20 @@ function errorResultUnexpectedError(actionId: string): ConnectorTypeExecutorResu
|
|||
};
|
||||
}
|
||||
|
||||
function errorResultUnexpectedNullResponse(actionId: string): ConnectorTypeExecutorResult<void> {
|
||||
const message = i18n.translate(
|
||||
'xpack.stackConnectors.webhook.unexpectedNullResponseErrorMessage',
|
||||
{
|
||||
defaultMessage: 'unexpected null response from webhook',
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function retryResult(actionId: string, serviceMessage: string): ConnectorTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.stackConnectors.webhook.invalidResponseRetryLaterErrorMessage',
|
||||
|
|
|
@ -274,6 +274,20 @@ export async function executor(
|
|||
};
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
const message = i18n.translate(
|
||||
'xpack.stackConnectors.xmatters.unexpectedNullResponseErrorMessage',
|
||||
{
|
||||
defaultMessage: 'unexpected null response from xmatters',
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
const { status, statusText } = result;
|
||||
logger.debug(`Response from xMatters action "${actionId}": [HTTP ${status}] ${statusText}`);
|
||||
|
|
|
@ -456,7 +456,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to create case. Error: JSON Error: Create case JSON body must be valid JSON. ',
|
||||
|
@ -486,7 +486,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: JSON Error: Update case JSON body must be valid JSON. ',
|
||||
|
@ -553,7 +553,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ',
|
||||
|
@ -620,7 +620,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: Error: Invalid protocol. ',
|
||||
|
@ -650,7 +650,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: Error: Invalid URL. ',
|
||||
|
@ -717,7 +717,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: Error: Invalid URL. ',
|
||||
|
|
|
@ -181,7 +181,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message: `Sub action "invalidAction" is not registered. Connector id: ${opsgenieActionId}. Connector name: Opsgenie. Connector type: .opsgenie`,
|
||||
});
|
||||
|
@ -199,7 +199,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [message]: expected value of type [string] but got [undefined])',
|
||||
|
@ -218,7 +218,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [alias]: expected value of type [string] but got [undefined])',
|
||||
|
@ -250,7 +250,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [responders.0]: types that failed validation:\n- [responders.0.0.type]: types that failed validation:\n - [responders.0.type.0]: expected value to equal [team]\n - [responders.0.type.1]: expected value to equal [user]\n - [responders.0.type.2]: expected value to equal [escalation]\n - [responders.0.type.3]: expected value to equal [schedule]\n- [responders.0.1.id]: expected value of type [string] but got [undefined])',
|
||||
|
@ -279,7 +279,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [responders.0]: types that failed validation:\n- [responders.0.0.name]: expected value of type [string] but got [undefined]\n- [responders.0.1.id]: expected value of type [string] but got [undefined])',
|
||||
|
@ -381,7 +381,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [visibleTo.0]: types that failed validation:\n- [visibleTo.0.0.type]: expected value to equal [team]\n- [visibleTo.0.1.id]: expected value of type [string] but got [undefined]\n- [visibleTo.0.2.id]: expected value of type [string] but got [undefined]\n- [visibleTo.0.3.username]: expected value of type [string] but got [undefined])',
|
||||
|
@ -445,7 +445,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
connector_id: opsgenieActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [details.bananas]: expected value of type [string] but got [number])',
|
||||
|
@ -680,7 +680,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: opsgenieActionId,
|
||||
service_message: 'Status code: undefined. Message: Message: failed',
|
||||
});
|
||||
|
@ -702,7 +702,7 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
expect(body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: opsgenieActionId,
|
||||
service_message: 'Status code: undefined. Message: Message: failed',
|
||||
});
|
||||
|
|
|
@ -166,7 +166,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
expect(execRes.body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: res.body.id,
|
||||
service_message:
|
||||
'Request validation failed (Error: [id]: expected value of type [string] but got [undefined])',
|
||||
|
@ -245,7 +245,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
expect(execRes.body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: res.body.id,
|
||||
service_message: `Sub action \"notRegistered\" is not registered. Connector id: ${res.body.id}. Connector name: Test: Sub action connector. Connector type: .test-sub-action-connector`,
|
||||
});
|
||||
|
@ -265,7 +265,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
expect(execRes.body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: res.body.id,
|
||||
service_message: `Method \"notAFunction\" does not exists in service. Sub action: \"notAFunction\". Connector id: ${res.body.id}. Connector name: Test: Sub action connector. Connector type: .test-sub-action-connector`,
|
||||
});
|
||||
|
@ -285,7 +285,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
expect(execRes.body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: res.body.id,
|
||||
service_message: `Method \"notExist\" does not exists in service. Sub action: \"notExist\". Connector id: ${res.body.id}. Connector name: Test: Sub action connector. Connector type: .test-sub-action-connector`,
|
||||
});
|
||||
|
@ -308,7 +308,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
expect(execRes.body).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
retry: false,
|
||||
retry: true,
|
||||
connector_id: res.body.id,
|
||||
service_message: 'You should register at least one subAction for your connector type',
|
||||
});
|
||||
|
|
|
@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1);
|
||||
});
|
||||
|
||||
it('should cleanup task after a failure', async () => {
|
||||
it('should retry task after a failure', async () => {
|
||||
const testStart = new Date().toISOString();
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
|
@ -85,6 +85,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
|
||||
|
||||
const reference = `actions-enqueue-2:${Spaces.space1.id}:${createdAction.id}`;
|
||||
let runAt: number;
|
||||
await supertest
|
||||
.post(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action`
|
||||
|
@ -96,7 +97,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
index: ES_TEST_INDEX_NAME,
|
||||
},
|
||||
})
|
||||
.expect(204);
|
||||
.expect(204)
|
||||
.then(() => {
|
||||
runAt = Date.now();
|
||||
});
|
||||
await esTestIndexTool.waitForDocs('action:test.failing', reference, 1);
|
||||
|
||||
await retry.try(async () => {
|
||||
|
@ -123,7 +127,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect((searchResult.hits.total as estypes.SearchTotalHits).value).to.eql(0);
|
||||
const hit = searchResult.hits.hits as Array<estypes.SearchHit<any>>;
|
||||
expect(Date.parse(hit[0]._source.task.runAt)).to.greaterThan(runAt);
|
||||
expect(Date.parse(hit[0]._source.task.attempts)).to.greaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
service_message: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`,
|
||||
retry: false,
|
||||
retry: true,
|
||||
});
|
||||
|
||||
await validateEventLog({
|
||||
|
@ -142,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
actionTypeId: 'test.failing',
|
||||
outcome: 'failure',
|
||||
message: `action execution failure: test.failing:${createdAction.id}: failing action`,
|
||||
errorMessage: `an error occurred while running the action: expected failure for .kibana-alerting-test-data actions-failure-1:space1`,
|
||||
errorMessage: `an error occurred while running the action: expected failure for .kibana-alerting-test-data actions-failure-1:space1; retry: true`,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -327,7 +327,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`,
|
||||
retry: false,
|
||||
retry: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,7 +115,7 @@ export default function createGetActionErrorLogTests({ getService }: FtrProvider
|
|||
for (const errors of response.body.errors) {
|
||||
expect(errors.type).to.equal('actions');
|
||||
expect(errors.message).to.equal(
|
||||
`action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail`
|
||||
`action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail; retry: true`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue