From c11c7d2a9920396bad62bef2cf737771a1cd0514 Mon Sep 17 00:00:00 2001 From: bespaltovyj <32296746+bespaltovyj@users.noreply.github.com> Date: Mon, 18 Oct 2021 10:34:30 +0400 Subject: [PATCH] feat(engine): support composite incident handlers * adds new engine flag for enabling composite incident handlers so multiple incident handlers can be triggered for the same type related to CAM-13569 closes #1561 --- .../cfg/ProcessEngineConfigurationImpl.java | 51 +++- .../incident/CompositeIncidentHandler.java | 125 ++++++++++ .../CompositeIncidentHandlerTest.java | 222 ++++++++++++++++++ .../mgmt/IncidentMultipleProcessingTest.java | 192 +++++++++++++++ 4 files changed, 586 insertions(+), 4 deletions(-) create mode 100644 engine/src/main/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandler.java create mode 100644 engine/src/test/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandlerTest.java create mode 100644 engine/src/test/java/org/camunda/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java diff --git a/engine/src/main/java/org/camunda/bpm/engine/impl/cfg/ProcessEngineConfigurationImpl.java b/engine/src/main/java/org/camunda/bpm/engine/impl/cfg/ProcessEngineConfigurationImpl.java index 1b5abe74b6f..9f2e6f242e6 100644 --- a/engine/src/main/java/org/camunda/bpm/engine/impl/cfg/ProcessEngineConfigurationImpl.java +++ b/engine/src/main/java/org/camunda/bpm/engine/impl/cfg/ProcessEngineConfigurationImpl.java @@ -210,6 +210,7 @@ import org.camunda.bpm.engine.impl.identity.ReadOnlyIdentityProvider; import org.camunda.bpm.engine.impl.identity.WritableIdentityProvider; import org.camunda.bpm.engine.impl.identity.db.DbIdentityServiceProvider; +import org.camunda.bpm.engine.impl.incident.CompositeIncidentHandler; import org.camunda.bpm.engine.impl.incident.DefaultIncidentHandler; import org.camunda.bpm.engine.impl.incident.IncidentHandler; import org.camunda.bpm.engine.impl.interceptor.CommandContextFactory; @@ -761,6 +762,22 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig protected boolean isExecutionTreePrefetchEnabled = true; + /** + * If true, the incident handlers init as {@link CompositeIncidentHandler} and + * multiple incident handlers can be added for the same Incident type. + * However, only the result from the "main" incident handler will be returned. + *

+ * All {@link customIncidentHandlers} will be added as sub handlers to {@link CompositeIncidentHandler} for same handler type. + *

+ * By default, main handler is {@link DefaultIncidentHandler}. + * To override the main handler you need create {@link CompositeIncidentHandler} with your main IncidentHandler and + * init {@link incidentHandlers} before setting up the engine. + * + * @see CompositeIncidentHandler + * @see #initIncidentHandlers + */ + protected boolean isCompositeIncidentHandlersEnabled = false; + /** * If true the process engine will attempt to acquire an exclusive lock before * creating a deployment. @@ -1325,14 +1342,21 @@ protected void initIncidentHandlers() { incidentHandlers = new HashMap<>(); DefaultIncidentHandler failedJobIncidentHandler = new DefaultIncidentHandler(Incident.FAILED_JOB_HANDLER_TYPE); - incidentHandlers.put(failedJobIncidentHandler.getIncidentHandlerType(), failedJobIncidentHandler); + DefaultIncidentHandler failedExternalTaskIncidentHandler = new DefaultIncidentHandler( + Incident.EXTERNAL_TASK_HANDLER_TYPE); + + if (isCompositeIncidentHandlersEnabled) { + addIncidentHandler(new CompositeIncidentHandler(failedJobIncidentHandler)); + addIncidentHandler(new CompositeIncidentHandler(failedExternalTaskIncidentHandler)); + } else { + addIncidentHandler(failedJobIncidentHandler); + addIncidentHandler(failedExternalTaskIncidentHandler); + } - DefaultIncidentHandler failedExternalTaskIncidentHandler = new DefaultIncidentHandler(Incident.EXTERNAL_TASK_HANDLER_TYPE); - incidentHandlers.put(failedExternalTaskIncidentHandler.getIncidentHandlerType(), failedExternalTaskIncidentHandler); } if (customIncidentHandlers != null) { for (IncidentHandler incidentHandler : customIncidentHandlers) { - incidentHandlers.put(incidentHandler.getIncidentHandlerType(), incidentHandler); + addIncidentHandler(incidentHandler); } } } @@ -3791,6 +3815,16 @@ public IncidentHandler getIncidentHandler(String incidentType) { return incidentHandlers.get(incidentType); } + public void addIncidentHandler(IncidentHandler incidentHandler) { + IncidentHandler existsHandler = incidentHandlers.get(incidentHandler.getIncidentHandlerType()); + + if (existsHandler instanceof CompositeIncidentHandler) { + ((CompositeIncidentHandler) existsHandler).add(incidentHandler); + } else { + incidentHandlers.put(incidentHandler.getIncidentHandlerType(), incidentHandler); + } + } + public Map getIncidentHandlers() { return incidentHandlers; } @@ -4114,6 +4148,15 @@ public ProcessEngineConfigurationImpl setStandaloneTasksEnabled(boolean standalo return this; } + public boolean isCompositeIncidentHandlersEnabled() { + return isCompositeIncidentHandlersEnabled; + } + + public ProcessEngineConfigurationImpl setCompositeIncidentHandlersEnabled(boolean compositeIncidentHandlersEnabled) { + this.isCompositeIncidentHandlersEnabled = compositeIncidentHandlersEnabled; + return this; + } + public ScriptFactory getScriptFactory() { return scriptFactory; } diff --git a/engine/src/main/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandler.java b/engine/src/main/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandler.java new file mode 100644 index 00000000000..602f9f4bf06 --- /dev/null +++ b/engine/src/main/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.camunda.bpm.engine.impl.incident; + +import org.camunda.bpm.engine.ProcessEngineException; +import org.camunda.bpm.engine.impl.util.EnsureUtil; +import org.camunda.bpm.engine.runtime.Incident; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + *

+ * A composite incident handler that handles incidents of a certain type by the multiple handlers. + * The result of handling depends on main handler. + * + * @see #mainIncidentHandler + *

+ * @see IncidentHandler + */ +public class CompositeIncidentHandler implements IncidentHandler { + + protected IncidentHandler mainIncidentHandler; + protected final List incidentHandlers = new ArrayList<>(); + + /** + * Constructor that takes a list of {@link IncidentHandler} that consume + * the incident. + * + * @param mainIncidentHandler the main incident handler {@link IncidentHandler} that consume the incident and return result. + * @param incidentHandlers the list of {@link IncidentHandler} that consume the incident. + */ + public CompositeIncidentHandler(IncidentHandler mainIncidentHandler, final List incidentHandlers) { + initializeIncidentsHandlers(mainIncidentHandler, incidentHandlers); + } + + /** + * Constructor that takes a varargs parameter {@link IncidentHandler} that + * consume the incident. + * + * @param mainIncidentHandler the main incident handler {@link IncidentHandler} that consume the incident and return result. + * @param incidentHandlers the list of {@link IncidentHandler} that consume the incident. + */ + public CompositeIncidentHandler(IncidentHandler mainIncidentHandler, final IncidentHandler... incidentHandlers) { + EnsureUtil.ensureNotNull("Incident handlers", (Object[]) incidentHandlers); + initializeIncidentsHandlers(mainIncidentHandler, Arrays.asList(incidentHandlers)); + } + + /** + * Initialize {@link #incidentHandlers} with data transfered from constructor + * + * @param incidentHandlers + */ + protected void initializeIncidentsHandlers(IncidentHandler mainIncidentHandler, + final List incidentHandlers) { + EnsureUtil.ensureNotNull("Incident handler", mainIncidentHandler); + this.mainIncidentHandler = mainIncidentHandler; + + EnsureUtil.ensureNotNull("Incident handlers", incidentHandlers); + for (IncidentHandler incidentHandler : incidentHandlers) { + add(incidentHandler); + } + } + + /** + * Adds the {@link IncidentHandler} to the list of + * {@link IncidentHandler} that consume the incident. + * + * @param incidentHandler the {@link IncidentHandler} that consume the incident. + */ + public void add(final IncidentHandler incidentHandler) { + EnsureUtil.ensureNotNull("Incident handler", incidentHandler); + String incidentHandlerType = getIncidentHandlerType(); + if (!incidentHandlerType.equals(incidentHandler.getIncidentHandlerType())) { + throw new ProcessEngineException( + "Incorrect incident type handler in composite handler with type: " + incidentHandlerType); + } + this.incidentHandlers.add(incidentHandler); + } + + @Override + public String getIncidentHandlerType() { + return mainIncidentHandler.getIncidentHandlerType(); + } + + @Override + public Incident handleIncident(IncidentContext context, String message) { + Incident incident = mainIncidentHandler.handleIncident(context, message); + for (IncidentHandler incidentHandler : this.incidentHandlers) { + incidentHandler.handleIncident(context, message); + } + return incident; + } + + @Override + public void resolveIncident(IncidentContext context) { + mainIncidentHandler.resolveIncident(context); + for (IncidentHandler incidentHandler : this.incidentHandlers) { + incidentHandler.resolveIncident(context); + } + } + + @Override + public void deleteIncident(IncidentContext context) { + mainIncidentHandler.deleteIncident(context); + for (IncidentHandler incidentHandler : this.incidentHandlers) { + incidentHandler.deleteIncident(context); + } + } +} diff --git a/engine/src/test/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandlerTest.java b/engine/src/test/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandlerTest.java new file mode 100644 index 00000000000..ba5134ef680 --- /dev/null +++ b/engine/src/test/java/org/camunda/bpm/engine/impl/incident/CompositeIncidentHandlerTest.java @@ -0,0 +1,222 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.camunda.bpm.engine.impl.incident; + +import org.camunda.bpm.engine.ProcessEngineException; +import org.camunda.bpm.engine.exception.NullValueException; +import org.camunda.bpm.engine.runtime.Incident; +import org.junit.Test; +import org.mockito.internal.verification.Times; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CompositeIncidentHandlerTest { + + @Test + public void shouldUseCompositeIncidentHandlerWithMainIncidentHandlerAddNullHandler() { + CompositeIncidentHandler compositeIncidentHandler = new CompositeIncidentHandler(new DefaultIncidentHandler("")); + try { + compositeIncidentHandler.add(null); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handler is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithNullMainHandler() { + try { + new CompositeIncidentHandler(null); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handler is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithNullVarargs() { + IncidentHandler incidentHandler = null; + try { + new CompositeIncidentHandler(null, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handlers contains null value"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithNullList() { + List incidentHandler = null; + try { + new CompositeIncidentHandler(null, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handler is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithMainHandlersAndNullVarargValue() { + IncidentHandler mainIncidentHandler = new DefaultIncidentHandler("failedJob"); + IncidentHandler incidentHandler = null; + try { + new CompositeIncidentHandler(mainIncidentHandler, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handlers contains null value"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithMainHandlersAndNullVarargs() { + IncidentHandler mainIncidentHandler = new DefaultIncidentHandler("failedJob"); + IncidentHandler[] incidentHandler = null; + try { + new CompositeIncidentHandler(mainIncidentHandler, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handlers is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithMainHandlersAndNullList() { + IncidentHandler mainIncidentHandler = new DefaultIncidentHandler("failedJob"); + + List incidentHandler = null; + try { + new CompositeIncidentHandler(mainIncidentHandler, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handlers is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerArgumentConstructorWithMainHandlersAndListWithNulls() { + IncidentHandler mainIncidentHandler = new DefaultIncidentHandler("failedJob"); + + List incidentHandler = new ArrayList<>(); + incidentHandler.add(null); + incidentHandler.add(null); + try { + new CompositeIncidentHandler(mainIncidentHandler, incidentHandler); + fail("NullValueException expected"); + } catch (NullValueException e) { + assertThat(e.getMessage()).containsIgnoringCase("Incident handler is null"); + } + } + + @Test + public void shouldUseCompositeIncidentHandlerWithAnotherIncidentType() { + CompositeIncidentHandler compositeIncidentHandler = new CompositeIncidentHandler( + new DefaultIncidentHandler("failedJob")); + try { + compositeIncidentHandler.add(new DefaultIncidentHandler("failedExternalTask")); + fail("Non expected message expected"); + } catch (ProcessEngineException e) { + assertThat(e.getMessage()).containsIgnoringCase( + "Incorrect incident type handler in composite handler with type: failedJob"); + } + } + + @Test + public void shouldCallAllHandlersWhenCreatingIncident() { + IncidentHandler mainHandler = mock(IncidentHandler.class); + Incident incident = mock(Incident.class); + + when(mainHandler.getIncidentHandlerType()).thenReturn("failedJob"); + when(mainHandler.handleIncident(any(), any())).thenReturn(incident); + + CompositeIncidentHandler compositeIncidentHandler = new CompositeIncidentHandler(mainHandler); + + IncidentHandler subHandler = mock(IncidentHandler.class); + + when(subHandler.getIncidentHandlerType()).thenReturn("failedJob"); + when(subHandler.handleIncident(any(), any())).thenReturn(null); + + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + + IncidentContext incidentContext = mock(IncidentContext.class); + + Incident result = compositeIncidentHandler.handleIncident(incidentContext, "Incident message"); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(incident); + + verify(mainHandler).handleIncident(eq(incidentContext), eq("Incident message")); + verify(subHandler, new Times(3)).handleIncident(eq(incidentContext), eq("Incident message")); + } + + @Test + public void shouldCallAllHandlersWhenDeletingIncident() { + IncidentHandler mainHandler = mock(IncidentHandler.class); + + when(mainHandler.getIncidentHandlerType()).thenReturn("failedJob"); + + CompositeIncidentHandler compositeIncidentHandler = new CompositeIncidentHandler(mainHandler); + + IncidentHandler subHandler = mock(IncidentHandler.class); + when(subHandler.getIncidentHandlerType()).thenReturn("failedJob"); + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + + IncidentContext incidentContext = mock(IncidentContext.class); + + compositeIncidentHandler.deleteIncident(incidentContext); + + verify(mainHandler).deleteIncident(eq(incidentContext)); + verify(subHandler, new Times(3)).deleteIncident(eq(incidentContext)); + } + + @Test + public void shouldCallAllHandlersWhenResolvingIncident() { + IncidentHandler mainHandler = mock(IncidentHandler.class); + + when(mainHandler.getIncidentHandlerType()).thenReturn("failedJob"); + + CompositeIncidentHandler compositeIncidentHandler = new CompositeIncidentHandler(mainHandler); + + IncidentHandler subHandler = mock(IncidentHandler.class); + + when(subHandler.getIncidentHandlerType()).thenReturn("failedJob"); + + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + compositeIncidentHandler.add(subHandler); + + IncidentContext incidentContext = mock(IncidentContext.class); + + compositeIncidentHandler.resolveIncident(incidentContext); + + verify(mainHandler).resolveIncident(eq(incidentContext)); + verify(subHandler, new Times(3)).resolveIncident(eq(incidentContext)); + } +} + diff --git a/engine/src/test/java/org/camunda/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java b/engine/src/test/java/org/camunda/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java new file mode 100644 index 00000000000..adc63d47544 --- /dev/null +++ b/engine/src/test/java/org/camunda/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java @@ -0,0 +1,192 @@ +package org.camunda.bpm.engine.test.api.mgmt; + +import org.camunda.bpm.engine.ManagementService; +import org.camunda.bpm.engine.RuntimeService; +import org.camunda.bpm.engine.impl.incident.CompositeIncidentHandler; +import org.camunda.bpm.engine.impl.incident.IncidentContext; +import org.camunda.bpm.engine.impl.incident.IncidentHandler; +import org.camunda.bpm.engine.runtime.Incident; +import org.camunda.bpm.engine.runtime.Job; +import org.camunda.bpm.engine.runtime.ProcessInstance; +import org.camunda.bpm.engine.test.Deployment; +import org.camunda.bpm.engine.test.ProcessEngineRule; +import org.camunda.bpm.engine.test.util.ProcessEngineBootstrapRule; +import org.camunda.bpm.engine.test.util.ProcessEngineTestRule; +import org.camunda.bpm.engine.test.util.ProvidedProcessEngineRule; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IncidentMultipleProcessingTest { + + private static final StubIncidentHandler JOB_HANDLER = new StubIncidentHandler(Incident.FAILED_JOB_HANDLER_TYPE); + + @ClassRule + public static ProcessEngineBootstrapRule processEngineBootstrapRule = new ProcessEngineBootstrapRule( + configuration -> { + configuration.setCompositeIncidentHandlersEnabled(true); + configuration.setCustomIncidentHandlers(Collections.singletonList(JOB_HANDLER)); + }); + protected ProcessEngineRule engineRule = new ProvidedProcessEngineRule(processEngineBootstrapRule); + protected ProcessEngineTestRule testRule = new ProcessEngineTestRule(engineRule); + + @Rule + public RuleChain ruleChain = RuleChain.outerRule(engineRule).around(testRule); + + private RuntimeService runtimeService; + private ManagementService managementService; + + @Before + public void init() { + JOB_HANDLER.reset(); + + runtimeService = engineRule.getRuntimeService(); + managementService = engineRule.getManagementService(); + } + + @Test + public void jobHandlerShouldBeCompositeHandler() { + IncidentHandler incidentHandler = engineRule.getProcessEngineConfiguration().getIncidentHandler(Incident.FAILED_JOB_HANDLER_TYPE); + + assertThat(incidentHandler).isNotNull(); + assertThat(incidentHandler).isInstanceOf(CompositeIncidentHandler.class); + } + + @Test + public void externalTaskHandlerShouldBeCompositeHandler() { + IncidentHandler incidentHandler = engineRule.getProcessEngineConfiguration().getIncidentHandler(Incident.EXTERNAL_TASK_HANDLER_TYPE); + + assertThat(incidentHandler).isNotNull(); + assertThat(incidentHandler).isInstanceOf(CompositeIncidentHandler.class); + } + + @Deployment(resources = { "org/camunda/bpm/engine/test/api/mgmt/IncidentTest.testShouldCreateOneIncident.bpmn" }) + @Test + public void shouldCreateOneIncident() { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess"); + + testRule.executeAvailableJobs(); + + List incidents = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).list(); + + assertThat(incidents).hasSize(1); + + assertThat(JOB_HANDLER.getCreateEvents()).hasSize(1); + assertThat(JOB_HANDLER.getResolveEvents()).isEmpty(); + assertThat(JOB_HANDLER.getDeleteEvents()).isEmpty(); + } + + @Deployment(resources = { "org/camunda/bpm/engine/test/api/mgmt/IncidentTest.testShouldCreateOneIncident.bpmn" }) + @Test + public void shouldResolveIncidentAfterJobRetriesRefresh() { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess"); + + testRule.executeAvailableJobs(); + + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + Incident incident = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(incident).isNotNull(); + + managementService.setJobRetries(job.getId(), 1); + + incident = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(incident).isNull(); + + assertThat(JOB_HANDLER.getCreateEvents()).hasSize(1); + // incidents resolved when job retries update + assertThat(JOB_HANDLER.getResolveEvents()).hasSize(1); + assertThat(JOB_HANDLER.getDeleteEvents()).isEmpty(); + } + + @Deployment(resources = { "org/camunda/bpm/engine/test/api/mgmt/IncidentTest.testShouldCreateOneIncident.bpmn" }) + @Test + public void shouldDeleteIncidentAfterJobHasBeenDeleted() { + // start failing process + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess"); + + testRule.executeAvailableJobs(); + + // get the job + Job job = managementService.createJobQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(job).isNotNull(); + + // there exists one incident to failed + Incident incident = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(incident).isNotNull(); + + // delete the job + managementService.deleteJob(job.getId()); + + // the incident has been deleted too. + incident = runtimeService.createIncidentQuery().incidentId(incident.getId()).singleResult(); + assertThat(incident).isNull(); + + assertThat(JOB_HANDLER.getCreateEvents()).hasSize(1); + assertThat(JOB_HANDLER.getResolveEvents()).isEmpty(); + assertThat(JOB_HANDLER.getDeleteEvents()).hasSize(1); + } + + public static class StubIncidentHandler implements IncidentHandler { + + private String incidentType; + + private List createEvents = new ArrayList<>(); + private List resolveEvents = new ArrayList<>(); + private List deleteEvents = new ArrayList<>(); + + public StubIncidentHandler(String type) { + this.incidentType = type; + } + + @Override + public String getIncidentHandlerType() { + return incidentType; + } + + @Override + public Incident handleIncident(IncidentContext context, String message) { + createEvents.add(context); + return null; + } + + @Override + public void resolveIncident(IncidentContext context) { + resolveEvents.add(context); + } + + @Override + public void deleteIncident(IncidentContext context) { + deleteEvents.add(context); + } + + public List getCreateEvents() { + return createEvents; + } + + public List getResolveEvents() { + return resolveEvents; + } + + public List getDeleteEvents() { + return deleteEvents; + } + + public void reset() { + createEvents.clear(); + resolveEvents.clear(); + deleteEvents.clear(); + } + + } + +}