diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 07c4d0371c718..abb8c52f9c8eb 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -40,6 +40,7 @@ import { omit } from 'lodash'; import { UntypedNormalizedAlertType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { ExecuteOptions } from '../../../actions/server/create_execute_function'; +import moment from 'moment'; const alertType: jest.Mocked = { id: 'test', @@ -55,6 +56,10 @@ const alertType: jest.Mocked = { let fakeTimer: sinon.SinonFakeTimers; +export const mockRunNowResponse = { + id: 1, +} as jest.ResolvedValue; + describe('Task Runner', () => { let mockedTaskInstance: ConcreteTaskInstance; @@ -865,7 +870,136 @@ describe('Task Runner', () => { } ); - test('actionsPlugin.execute is not called when notifyWhen=onActionGroupChange and alert instance state does not change', async () => { + testAgainstEphemeralSupport( + 'skips firing actions for active alert if alert is throttled %s', + ( + customTaskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType, + enqueueFunction: (options: ExecuteOptions) => Promise + ) => + async () => { + ( + customTaskRunnerFactoryInitializerParams as TaskRunnerFactoryInitializerParamsType + ).actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue( + true + ); + actionsClient.ephemeralEnqueuedExecution.mockResolvedValue(mockRunNowResponse); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '2': { + meta: { + lastScheduledActions: { date: moment().toISOString(), group: 'default' }, + }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 86400000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + rulesClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + throttle: '1d', + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, + }, + references: [], + }); + await taskRunner.run(); + // expect(enqueueFunction).toHaveBeenCalledTimes(1); + + const logger = customTaskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith( + 3, + `skipping scheduling of actions for '2' in alert test:1: 'alert-name': instance is throttled` + ); + } + ); + + testAgainstEphemeralSupport( + 'skips firing actions for active alert when alert is muted even if notifyWhen === onActionGroupChange %s', + ( + customTaskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType, + enqueueFunction: (options: ExecuteOptions) => Promise + ) => + async () => { + customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue( + true + ); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + customTaskRunnerFactoryInitializerParams + ); + rulesClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + mutedInstanceIds: ['2'], + notifyWhen: 'onActionGroupChange', + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + enabled: true, + }, + references: [], + }); + await taskRunner.run(); + expect(enqueueFunction).toHaveBeenCalledTimes(1); + const logger = customTaskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith( + 3, + `skipping scheduling of actions for '2' in alert test:1: 'alert-name': instance is muted` + ); + } + ); + + test('actionsPlugin.execute is not called when notifyWhen=onActionGroupChange and alert state does not change', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 642a2c59d30f3..88ee9d3e637e8 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -379,41 +379,35 @@ export class TaskRunner< alertLabel, }); - const instancesToExecute = - notifyWhen === 'onActionGroupChange' - ? Object.entries(instancesWithScheduledActions).filter( - ([alertInstanceName, alertInstance]: [ - string, - AlertInstance - ]) => { - const shouldExecuteAction = - alertInstance.scheduledActionGroupOrSubgroupHasChanged(); - if (!shouldExecuteAction) { - this.logger.debug( - `skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is active but action group has not changed` - ); - } - return shouldExecuteAction; - } - ) - : Object.entries(instancesWithScheduledActions).filter( - ([alertInstanceName, alertInstance]: [ - string, - AlertInstance - ]) => { - const throttled = alertInstance.isThrottled(throttle); - const muted = mutedInstanceIdsSet.has(alertInstanceName); - const shouldExecuteAction = !throttled && !muted; - if (!shouldExecuteAction) { - this.logger.debug( - `skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is ${ - muted ? 'muted' : 'throttled' - }` - ); - } - return shouldExecuteAction; - } + const instancesToExecute = Object.entries(instancesWithScheduledActions).filter( + ([alertInstanceName, alertInstance]: [ + string, + AlertInstance + ]) => { + const throttled = alertInstance.isThrottled(throttle); + const muted = mutedInstanceIdsSet.has(alertInstanceName); + let shouldExecuteAction = true; + + if (throttled || muted) { + shouldExecuteAction = false; + this.logger.debug( + `skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is ${ + muted ? 'muted' : 'throttled' + }` ); + } else if ( + notifyWhen === 'onActionGroupChange' && + !alertInstance.scheduledActionGroupOrSubgroupHasChanged() + ) { + shouldExecuteAction = false; + this.logger.debug( + `skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is active but action group has not changed` + ); + } + + return shouldExecuteAction; + } + ); await Promise.all( instancesToExecute.map(