diff --git a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/cmd/JobRetryCmd.java b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/cmd/JobRetryCmd.java index 3b32662c240..7545bb283f7 100755 --- a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/cmd/JobRetryCmd.java +++ b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/cmd/JobRetryCmd.java @@ -52,6 +52,7 @@ protected void logException(JobEntity job) { if(exception != null) { job.setExceptionMessage(exception.getMessage()); job.setExceptionStacktrace(getExceptionStacktrace()); + job.setException(this.exception); } } diff --git a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/incident/IncidentContext.java b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/incident/IncidentContext.java index a8f0253ddd3..e7f9b1fe04d 100644 --- a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/incident/IncidentContext.java +++ b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/incident/IncidentContext.java @@ -31,6 +31,7 @@ public class IncidentContext { protected String jobDefinitionId; protected String historyConfiguration; protected String failedActivityId; + private transient Throwable throwable; public IncidentContext() {} @@ -107,4 +108,12 @@ public void setFailedActivityId(String failedActivityId) { this.failedActivityId = failedActivityId; } + public Throwable getThrowable() { + return throwable; + } + + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + } + } diff --git a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/persistence/entity/JobEntity.java b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/persistence/entity/JobEntity.java index 2d5ac13150a..81116216e49 100644 --- a/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/persistence/entity/JobEntity.java +++ b/engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/persistence/entity/JobEntity.java @@ -116,6 +116,9 @@ public abstract class JobEntity extends AcquirableJobEntity protected String batchId; + // transient fields + private transient Throwable exception; + public void execute(CommandContext commandContext) { if (executionId != null) { ExecutionEntity execution = getExecution(); @@ -379,6 +382,7 @@ protected void createFailedJobIncident() { incidentContext.setActivityId(getActivityId()); incidentContext.setHistoryConfiguration(getLastFailureLogId()); incidentContext.setFailedActivityId(getFailedActivityId()); + incidentContext.setThrowable(this.getException()); IncidentHandling.createIncident(incidentHandlerType, incidentContext, exceptionMessage); @@ -486,6 +490,14 @@ public String getExceptionMessage() { return exceptionMessage; } + public Throwable getException() { + return exception; + } + + public void setException(Throwable exception) { + this.exception = exception; + } + @Override public String getJobDefinitionId() { return jobDefinitionId; @@ -555,6 +567,7 @@ protected void clearFailedJobException() { this.exceptionByteArrayId = null; this.exceptionMessage = null; + this.exception = null; } @Override diff --git a/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java b/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java index f7d8f2c6ba0..01f66c43b1c 100644 --- a/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java +++ b/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentMultipleProcessingTest.java @@ -100,6 +100,33 @@ public void shouldCreateOneIncident() { assertThat(JOB_HANDLER.getDeleteEvents()).isEmpty(); } + @Deployment(resources = { + "org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentTest.testShouldCreateOneIncident.bpmn" }) + @Test + public void checkThrowableOnIncidentCreationProcess() { + //given + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess"); + + //when + testRule.executeAvailableJobs(); + + List incidents = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).list(); + + //then + assertThat(incidents).hasSize(1); + + assertThat(JOB_HANDLER.getCreateEvents()).hasSize(1); + assertThat((JOB_HANDLER.getCreateEvents()).getFirst()).isNotNull(); + //The exception is exposed through the Incident Context while the Incident is handled. + assertThat((JOB_HANDLER.getCreateEvents()).getFirst().getThrowable()).isNotNull(); + //We assert that the actual message raised by the delegate is found as part of the Exception. + assertThat((JOB_HANDLER.getCreateEvents()).getFirst().getThrowable().getMessage()).contains( + AlwaysFailingDelegate.MESSAGE); + + assertThat(JOB_HANDLER.getResolveEvents()).isEmpty(); + assertThat(JOB_HANDLER.getDeleteEvents()).isEmpty(); + } + @Deployment(resources = { "org/finos/fluxnova/bpm/engine/test/api/mgmt/IncidentTest.testShouldCreateOneIncident.bpmn" }) @Test public void shouldResolveIncidentAfterJobRetriesRefresh() { diff --git a/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/jobexecutor/JobExecutorCmdExceptionTest.java b/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/jobexecutor/JobExecutorCmdExceptionTest.java index a6c98a2ae7f..3743c002a0a 100644 --- a/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/jobexecutor/JobExecutorCmdExceptionTest.java +++ b/engine/src/test/java/org/finos/fluxnova/bpm/engine/test/jobexecutor/JobExecutorCmdExceptionTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -29,10 +30,12 @@ import org.finos.fluxnova.bpm.engine.impl.db.DbEntity; import org.finos.fluxnova.bpm.engine.impl.interceptor.Command; import org.finos.fluxnova.bpm.engine.impl.interceptor.CommandContext; +import org.finos.fluxnova.bpm.engine.impl.persistence.entity.JobEntity; import org.finos.fluxnova.bpm.engine.impl.persistence.entity.MessageEntity; import org.finos.fluxnova.bpm.engine.runtime.Job; import org.finos.fluxnova.bpm.engine.runtime.JobQuery; import org.finos.fluxnova.bpm.engine.test.Deployment; +import org.finos.fluxnova.bpm.engine.test.api.mgmt.AlwaysFailingDelegate; import org.finos.fluxnova.bpm.engine.test.util.PluggableProcessEngineTest; import org.finos.fluxnova.bpm.model.bpmn.Bpmn; import org.junit.After; @@ -202,6 +205,41 @@ public void testFailingTransactionListener() { assertTrue("unexpected stacktrace, was <" + stacktrace + ">", stacktrace.contains("java.lang.RuntimeException: exception in transaction listener")); } + @Test + public void testTransientExceptionIsNotPersistedPostJobFailure() { + // given + testRule.deploy(Bpmn.createExecutableProcess("testProcess") + .fluxnovaHistoryTimeToLive(180) + .startEvent() + .serviceTask("theServiceTask") + .fluxnovaAsyncBefore() + .fluxnovaClass(AlwaysFailingDelegate.class) + .fluxnovaFailedJobRetryTimeCycle("R0/PT30S") + .endEvent() + .done()); + + runtimeService.startProcessInstanceByKey("testProcess"); + Job job = managementService.createJobQuery().singleResult(); + assertNotNull("Job should not be null", job); + assertNull(job.getExceptionMessage()); + assertNull(((JobEntity) job).getException()); + + // when + try { + managementService.executeJob(job.getId()); + } catch (Exception e) { + // expected exception + } + + // then + job = managementService.createJobQuery().singleResult(); + assertNotNull("Job should not be null", job); + //The message is obtained from the persistence layer. + assertEquals(AlwaysFailingDelegate.MESSAGE, job.getExceptionMessage()); + //The exception at this point is null but already propagated to the Incident Handler. + assertNull(((JobEntity) job).getException()); + } + protected void createJob(final String handlerType) { processEngineConfiguration.getCommandExecutorTxRequired().execute(new Command() {