/* Licensed under the Apache License, Version 2.0 (the "License"); * 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.activiti.engine.test.bpmn.subprocess.transaction; import java.util.List; import org.activiti.engine.impl.EventSubscriptionQueryImpl; import org.activiti.engine.impl.history.HistoryLevel; import org.activiti.engine.impl.persistence.entity.EventSubscriptionEntity; import org.activiti.engine.impl.test.PluggableActivitiTestCase; import org.activiti.engine.runtime.Execution; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; import org.activiti.engine.test.Deployment; /** * @author Daniel Meyer */ public class TransactionSubProcessTest extends PluggableActivitiTestCase { @Deployment(resources={"org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testSimpleCase.bpmn20.xml"}) public void testSimpleCaseTxSuccessful() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // after the process is started, we have compensate event subscriptions: assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); // the task is present: Task task = taskService.createTaskQuery().singleResult(); assertNotNull(task); // making the tx succeed: taskService.setVariable(task.getId(), "confirmed", true); taskService.complete(task.getId()); // now the process instance execution is sitting in the 'afterSuccess' task // -> has left the transaction using the "normal" sequence flow List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId()); assertTrue(activeActivityIds.contains("afterSuccess")); // there is a compensate event subscription for the transaction under the process instance EventSubscriptionEntity eventSubscriptionEntity = createEventSubscriptionQuery().eventType("compensate").activityId("tx").executionId(processInstance.getId()).singleResult(); // there is an event-scope execution associated with the event-subscription: assertNotNull(eventSubscriptionEntity.getConfiguration()); Execution eventScopeExecution = runtimeService.createExecutionQuery().executionId(eventSubscriptionEntity.getConfiguration()).singleResult(); assertNotNull(eventScopeExecution); // we still have compensate event subscriptions for the compensation handlers, only now they are part of the event scope assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").executionId(eventScopeExecution.getId()).count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").executionId(eventScopeExecution.getId()).count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("undoChargeCard").executionId(eventScopeExecution.getId()).count()); // assert that the compensation handlers have not been invoked: assertNull(runtimeService.getVariable(processInstance.getId(), "undoBookHotel")); assertNull(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")); assertNull(runtimeService.getVariable(processInstance.getId(), "undoChargeCard")); // end the process instance runtimeService.signal(processInstance.getId()); assertProcessEnded(processInstance.getId()); assertEquals(0, runtimeService.createExecutionQuery().count()); } @Deployment(resources={"org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testSimpleCase.bpmn20.xml"}) public void testSimpleCaseTxCancelled() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // after the process is started, we have compensate event subscriptions: assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); // the task is present: Task task = taskService.createTaskQuery().singleResult(); assertNotNull(task); // making the tx fail: taskService.setVariable(task.getId(), "confirmed", false); taskService.complete(task.getId()); // now the process instance execution is sitting in the 'afterCancellation' task // -> has left the transaction using the cancel boundary event List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId()); assertTrue(activeActivityIds.contains("afterCancellation")); // we have no more compensate event subscriptions assertEquals(0,createEventSubscriptionQuery().eventType("compensate").count()); // assert that the compensation handlers have been invoked: assertEquals(5, runtimeService.getVariable(processInstance.getId(), "undoBookHotel")); assertEquals(1, runtimeService.getVariable(processInstance.getId(), "undoBookFlight")); assertEquals(1, runtimeService.getVariable(processInstance.getId(), "undoChargeCard")); // if we have history, we check that the invocation of the compensation handlers is recorded in history. if(processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) { assertEquals(1, historyService.createHistoricActivityInstanceQuery() .activityId("undoBookFlight") .count()); assertEquals(5, historyService.createHistoricActivityInstanceQuery() .activityId("undoBookHotel") .count()); assertEquals(1, historyService.createHistoricActivityInstanceQuery() .activityId("undoChargeCard") .count()); } // end the process instance runtimeService.signal(processInstance.getId()); assertProcessEnded(processInstance.getId()); assertEquals(0, runtimeService.createExecutionQuery().count()); } @Deployment public void testCancelEndConcurrent() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // after the process is started, we have compensate event subscriptions: assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); // the task is present: Task task = taskService.createTaskQuery().singleResult(); assertNotNull(task); // making the tx fail: taskService.setVariable(task.getId(), "confirmed", false); taskService.complete(task.getId()); // now the process instance execution is sitting in the 'afterCancellation' task // -> has left the transaction using the cancel boundary event List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId()); assertTrue(activeActivityIds.contains("afterCancellation")); // we have no more compensate event subscriptions assertEquals(0,createEventSubscriptionQuery().eventType("compensate").count()); // assert that the compensation handlers have been invoked: assertEquals(5, runtimeService.getVariable(processInstance.getId(), "undoBookHotel")); assertEquals(1, runtimeService.getVariable(processInstance.getId(), "undoBookFlight")); // if we have history, we check that the invocation of the compensation handlers is recorded in history. if(processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) { assertEquals(1, historyService.createHistoricActivityInstanceQuery() .activityId("undoBookFlight") .count()); assertEquals(5, historyService.createHistoricActivityInstanceQuery() .activityId("undoBookHotel") .count()); } // end the process instance runtimeService.signal(processInstance.getId()); assertProcessEnded(processInstance.getId()); assertEquals(0, runtimeService.createExecutionQuery().count()); } @Deployment public void testNestedCancelInner() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // after the process is started, we have compensate event subscriptions: assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()); // the tasks are present: Task taskInner = taskService.createTaskQuery().taskDefinitionKey("innerTxaskCustomer").singleResult(); Task taskOuter = taskService.createTaskQuery().taskDefinitionKey("bookFlight").singleResult(); assertNotNull(taskInner); assertNotNull(taskOuter); // making the tx fail: taskService.setVariable(taskInner.getId(), "confirmed", false); taskService.complete(taskInner.getId()); // now the process instance execution is sitting in the 'afterInnerCancellation' task // -> has left the transaction using the cancel boundary event List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId()); assertTrue(activeActivityIds.contains("afterInnerCancellation")); // we have no more compensate event subscriptions for the inner tx assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()); assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()); // we do not have a subscription or the outer tx yet assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); // assert that the compensation handlers have been invoked: assertEquals(5, runtimeService.getVariable(processInstance.getId(), "innerTxundoBookHotel")); assertEquals(1, runtimeService.getVariable(processInstance.getId(), "innerTxundoBookFlight")); // if we have history, we check that the invocation of the compensation handlers is recorded in history. if(processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) { assertEquals(5, historyService.createHistoricActivityInstanceQuery() .activityId("innerTxundoBookHotel") .count()); assertEquals(1, historyService.createHistoricActivityInstanceQuery() .activityId("innerTxundoBookFlight") .count()); } // complete the task in the outer tx taskService.complete(taskOuter.getId()); // end the process instance (signal the execution still sitting in afterInnerCancellation) runtimeService.signal(runtimeService.createExecutionQuery().activityId("afterInnerCancellation").singleResult().getId()); assertProcessEnded(processInstance.getId()); assertEquals(0, runtimeService.createExecutionQuery().count()); } @Deployment public void testNestedCancelOuter() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // after the process is started, we have compensate event subscriptions: assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); assertEquals(5,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()); assertEquals(1,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()); // the tasks are present: Task taskInner = taskService.createTaskQuery().taskDefinitionKey("innerTxaskCustomer").singleResult(); Task taskOuter = taskService.createTaskQuery().taskDefinitionKey("bookFlight").singleResult(); assertNotNull(taskInner); assertNotNull(taskOuter); // making the outer tx fail (invokes cancel end event) taskService.complete(taskOuter.getId()); // now the process instance is sitting in 'afterOuterCancellation' List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId()); assertTrue(activeActivityIds.contains("afterOuterCancellation")); // we have no more compensate event subscriptions assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()); assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()); assertEquals(0,createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()); // the compensation handlers of the inner tx have not been invoked assertNull(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookHotel")); assertNull(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookFlight")); // the compensation handler in the outer tx has been invoked assertEquals(1, runtimeService.getVariable(processInstance.getId(), "undoBookFlight")); // end the process instance (signal the execution still sitting in afterOuterCancellation) runtimeService.signal(runtimeService.createExecutionQuery().activityId("afterOuterCancellation").singleResult().getId()); assertProcessEnded(processInstance.getId()); assertEquals(0, runtimeService.createExecutionQuery().count()); } /* * The cancel end event cancels all instances, compensation is performed for all instances * * see spec page 470: * "If the cancelActivity attribute is set, the Activity the Event is attached to is then * cancelled (in case of a multi-instance, all its instances are cancelled);" */ @Deployment public void testMultiInstanceTx() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // there are now 5 instances of the transaction: List<EventSubscriptionEntity> EventSubscriptionEntitys = createEventSubscriptionQuery() .eventType("compensate") .list(); // there are 10 compensation event subscriptions assertEquals(10, EventSubscriptionEntitys.size()); // the event subscriptions are all under the same execution (the execution of the multi-instance wrapper) String executionId = EventSubscriptionEntitys.get(0).getExecutionId(); for (EventSubscriptionEntity EventSubscriptionEntity : EventSubscriptionEntitys) { if(!executionId.equals(EventSubscriptionEntity.getExecutionId())) { fail("subscriptions not under same execution"); } } Task task = taskService.createTaskQuery().listPage(0, 1).get(0); // canceling one instance triggers compensation for all other instances: taskService.setVariable(task.getId(), "confirmed", false); taskService.complete(task.getId()); assertEquals(0, createEventSubscriptionQuery().count()); assertEquals(5, runtimeService.getVariable(processInstance.getId(), "undoBookHotel")); assertEquals(5, runtimeService.getVariable(processInstance.getId(), "undoBookFlight")); runtimeService.signal(runtimeService.createExecutionQuery().activityId("afterCancellation").singleResult().getId()); assertProcessEnded(processInstance.getId()); } @Deployment(resources={"org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testMultiInstanceTx.bpmn20.xml"}) public void testMultiInstanceTxSuccessful() { ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess"); // there are now 5 instances of the transaction: List<EventSubscriptionEntity> EventSubscriptionEntitys = createEventSubscriptionQuery() .eventType("compensate") .list(); // there are 10 compensation event subscriptions assertEquals(10, EventSubscriptionEntitys.size()); // the event subscriptions are all under the same execution (the execution of the multi-instance wrapper) String executionId = EventSubscriptionEntitys.get(0).getExecutionId(); for (EventSubscriptionEntity EventSubscriptionEntity : EventSubscriptionEntitys) { if(!executionId.equals(EventSubscriptionEntity.getExecutionId())) { fail("subscriptions not under same execution"); } } // first complete the inner user-tasks List<Task> tasks = taskService.createTaskQuery().list(); for (Task task : tasks) { taskService.setVariable(task.getId(), "confirmed", true); taskService.complete(task.getId()); } // now complete the inner receive tasks List<Execution> executions = runtimeService.createExecutionQuery().activityId("receive").list(); for (Execution execution : executions) { runtimeService.signal(execution.getId()); } runtimeService.signal(runtimeService.createExecutionQuery().activityId("afterSuccess").singleResult().getId()); assertEquals(0, createEventSubscriptionQuery().count()); assertProcessEnded(processInstance.getId()); } public void testMultipleCancelBoundaryFails() { try { repositoryService.createDeployment() .addClasspathResource("org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testMultipleCancelBoundaryFails.bpmn20.xml") .deploy(); fail("exception expected"); } catch (Exception e) { if(!e.getMessage().contains("multiple boundary events with cancelEventDefinition not supported on same transaction")) { fail("different exception expected"); } } } public void testCancelBoundaryNoTransactionFails() { try { repositoryService.createDeployment() .addClasspathResource("org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testCancelBoundaryNoTransactionFails.bpmn20.xml") .deploy(); fail("exception expected"); } catch (Exception e) { if(!e.getMessage().contains("boundary event with cancelEventDefinition only supported on transaction subprocesses")) { fail("different exception expected"); } } } public void testCancelEndNoTransactionFails() { try { repositoryService.createDeployment() .addClasspathResource("org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testCancelEndNoTransactionFails.bpmn20.xml") .deploy(); fail("exception expected"); } catch (Exception e) { if(!e.getMessage().contains("end event with cancelEventDefinition only supported inside transaction subprocess")) { fail("different exception expected"); } } } private EventSubscriptionQueryImpl createEventSubscriptionQuery() { return new EventSubscriptionQueryImpl(processEngineConfiguration.getCommandExecutorTxRequired()); } @Deployment public void testParseWithDI() { // this test simply makes sure we can parse a transaction subprocess with DI information // the actual transaction behavior is tested by other testcases //// failing case ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("TransactionSubProcessTest"); Task task = taskService.createTaskQuery().singleResult(); taskService.setVariable(task.getId(), "confirmed", false); taskService.complete(task.getId()); assertProcessEnded(processInstance.getId()); ////// success case processInstance = runtimeService.startProcessInstanceByKey("TransactionSubProcessTest"); task = taskService.createTaskQuery().singleResult(); taskService.setVariable(task.getId(), "confirmed", true); taskService.complete(task.getId()); assertProcessEnded(processInstance.getId()); } }