From b59a1f17456246214a1995ebf9c1313d0dab5fa9 Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Mon, 13 Nov 2023 12:01:12 +0100 Subject: [PATCH] Improve support for MI handling for process migration --- .../dynamic/AbstractDynamicStateManager.java | 152 +++- .../dynamic/MoveExecutionEntityContainer.java | 13 + .../dynamic/ProcessInstanceChangeState.java | 13 + .../entity/ExecutionEntityImpl.java | 2 +- ...essInstanceMigrationMultiInstanceTest.java | 756 +++++++++++++++++- .../ProcessInstanceMigrationTest.java | 4 +- ...nce-callactivity-local-variable.bpmn20.xml | 25 + ...llel-multi-instance-servicetask.bpmn20.xml | 25 + ...nstance-subprocess-callactivity.bpmn20.xml | 30 + ...tance-subprocess-local-variable.bpmn20.xml | 30 + ...instance-subprocess-servicetask.bpmn20.xml | 30 + ...ti-instance-task-local-variable.bpmn20.xml | 25 + 12 files changed, 1069 insertions(+), 36 deletions(-) create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-callactivity.bpmn20.xml create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java index e0401ae4b3f..4fbc39e12db 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java @@ -42,11 +42,13 @@ import org.flowable.bpmn.model.Gateway; import org.flowable.bpmn.model.IOParameter; import org.flowable.bpmn.model.MessageEventDefinition; +import org.flowable.bpmn.model.MultiInstanceLoopCharacteristics; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.Signal; import org.flowable.bpmn.model.SignalEventDefinition; import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.SubProcess; +import org.flowable.bpmn.model.Task; import org.flowable.bpmn.model.TimerEventDefinition; import org.flowable.bpmn.model.UserTask; import org.flowable.bpmn.model.ValuedDataObject; @@ -66,6 +68,7 @@ import org.flowable.engine.impl.delegate.ActivityBehavior; import org.flowable.engine.impl.dynamic.MoveExecutionEntityContainer.FlowElementMoveEntry; import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; +import org.flowable.engine.impl.jobexecutor.AsyncContinuationJobHandler; import org.flowable.engine.impl.jobexecutor.TimerEventHandler; import org.flowable.engine.impl.jobexecutor.TriggerTimerEventJobHandler; import org.flowable.engine.impl.persistence.deploy.DeploymentManager; @@ -81,6 +84,7 @@ import org.flowable.engine.impl.util.EntityLinkUtil; import org.flowable.engine.impl.util.Flowable5Util; import org.flowable.engine.impl.util.IOParameterUtil; +import org.flowable.engine.impl.util.JobUtil; import org.flowable.engine.impl.util.ProcessDefinitionUtil; import org.flowable.engine.impl.util.ProcessInstanceHelper; import org.flowable.engine.impl.util.TaskHelper; @@ -91,9 +95,11 @@ import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; import org.flowable.eventsubscription.service.impl.persistence.entity.SignalEventSubscriptionEntity; +import org.flowable.job.service.JobService; import org.flowable.job.service.TimerJobService; import org.flowable.job.service.impl.persistence.entity.DeadLetterJobEntityImpl; import org.flowable.job.service.impl.persistence.entity.ExternalWorkerJobEntityImpl; +import org.flowable.job.service.impl.persistence.entity.JobEntity; import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntityImpl; import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; import org.flowable.task.service.TaskService; @@ -116,7 +122,7 @@ public List resolveMoveExecutionEntityContainers(C List moveExecutionEntityContainerList = new ArrayList<>(); if (changeActivityStateBuilder.getMoveExecutionIdList().size() > 0) { for (MoveExecutionIdContainer executionContainer : changeActivityStateBuilder.getMoveExecutionIdList()) { - // Executions belonging to the same parent should move together - i.e multipleExecution to single activity + Map> executionsByParent = new HashMap<>(); Map> miExecutionsByParent = new HashMap<>(); for (String executionId : executionContainer.getExecutionIds()) { @@ -129,26 +135,56 @@ public List resolveMoveExecutionEntityContainers(C } executionEntities.add(execution); } - executionsByParent.values().forEach(executions -> { + + miExecutionsByParent.values().forEach(executions -> { MoveExecutionEntityContainer moveExecutionEntityContainer = new MoveExecutionEntityContainer(executions, executionContainer.getMoveToActivityIds()); + if (executions.get(0).getVariablesLocal() != null && !executions.get(0).getVariablesLocal().isEmpty()) { + moveExecutionEntityContainer.addLocalVariableMap(executions.get(0).getActivityId(), executions.get(0).getVariablesLocal()); + } if (executionContainer.getNewAssigneeId() != null) { - moveExecutionEntityContainer.setNewAssigneeId(executionContainer.getNewAssigneeId()); + moveExecutionEntityContainer.setNewAssigneeId(executionContainer.getNewAssigneeId()); } if (executionContainer.getNewOwnerId() != null) { - moveExecutionEntityContainer.setNewOwnerId(executionContainer.getNewOwnerId()); + moveExecutionEntityContainer.setNewOwnerId(executionContainer.getNewOwnerId()); } moveExecutionEntityContainerList.add(moveExecutionEntityContainer); }); - miExecutionsByParent.values().forEach(executions -> { - MoveExecutionEntityContainer moveExecutionEntityContainer = new MoveExecutionEntityContainer(executions, executionContainer.getMoveToActivityIds()); - if (executionContainer.getNewAssigneeId() != null) { - moveExecutionEntityContainer.setNewAssigneeId(executionContainer.getNewAssigneeId()); - } - if (executionContainer.getNewOwnerId() != null) { - moveExecutionEntityContainer.setNewOwnerId(executionContainer.getNewOwnerId()); + executionsByParent.values().forEach(executions -> { + if (!miExecutionsByParent.isEmpty() && executions.size() > 1 && (executions.get(0).getCurrentFlowElement() instanceof Task || executions.get(0).getCurrentFlowElement() instanceof CallActivity)) { + for (ExecutionEntity execution : executions) { + List miExecutionList = new ArrayList<>(); + miExecutionList.add(execution); + MoveExecutionEntityContainer moveExecutionEntityContainer = new MoveExecutionEntityContainer(miExecutionList, executionContainer.getMoveToActivityIds()); + + if (execution.getVariablesLocal() != null && !execution.getVariablesLocal().isEmpty()) { + moveExecutionEntityContainer.addLocalVariableMap(execution.getActivityId(), execution.getVariablesLocal()); + } + + if (executionContainer.getNewAssigneeId() != null) { + moveExecutionEntityContainer.setNewAssigneeId(executionContainer.getNewAssigneeId()); + } + if (executionContainer.getNewOwnerId() != null) { + moveExecutionEntityContainer.setNewOwnerId(executionContainer.getNewOwnerId()); + } + moveExecutionEntityContainerList.add(moveExecutionEntityContainer); + } + + } else { + MoveExecutionEntityContainer moveExecutionEntityContainer = new MoveExecutionEntityContainer(executions, executionContainer.getMoveToActivityIds()); + for (ExecutionEntity execution : executions) { + if (execution.getVariablesLocal() != null && !execution.getVariablesLocal().isEmpty()) { + moveExecutionEntityContainer.addLocalVariableMap(execution.getActivityId(), execution.getVariablesLocal()); + } + } + if (executionContainer.getNewAssigneeId() != null) { + moveExecutionEntityContainer.setNewAssigneeId(executionContainer.getNewAssigneeId()); + } + if (executionContainer.getNewOwnerId() != null) { + moveExecutionEntityContainer.setNewOwnerId(executionContainer.getNewOwnerId()); + } + moveExecutionEntityContainerList.add(moveExecutionEntityContainer); } - moveExecutionEntityContainerList.add(moveExecutionEntityContainer); }); } } @@ -498,8 +534,22 @@ protected void doMoveExecutionState(ProcessInstanceChangeState processInstanceCh CommandContextUtil.getAgenda(commandContext).planContinueProcessWithMigrationContextOperation(newChildExecution, migrationContext); } else { - CommandContextUtil.getAgenda(commandContext).planContinueProcessOperation(newChildExecution); - + if (newChildExecution.isMultiInstanceRoot() && (newChildExecution.getCurrentFlowElement() instanceof Task || newChildExecution.getCurrentFlowElement() instanceof CallActivity)) { + continue; + } + + if (newChildExecution.getCurrentFlowElement() instanceof Task && ((Task) newChildExecution.getCurrentFlowElement()).isAsynchronous()) { + JobService jobService = CommandContextUtil.getJobService(commandContext); + + JobEntity job = JobUtil.createJob(newChildExecution, newChildExecution.getCurrentFlowElement(), AsyncContinuationJobHandler.TYPE, CommandContextUtil.getProcessEngineConfiguration(commandContext)); + + Task task = (Task) newChildExecution.getCurrentFlowElement(); + jobService.createAsyncJob(job, task.isExclusive()); + jobService.scheduleAsyncJob(job); + + } else { + CommandContextUtil.getAgenda(commandContext).planContinueProcessOperation(newChildExecution); + } } } } @@ -662,7 +712,7 @@ protected List createEmbeddedSubProcessAndExecutions(Collection ExecutionEntity defaultContinueParentExecution = moveExecutionEntityContainer.getContinueParentExecution(movingExecutions.get(0).getId()); Set movingExecutionIds = movingExecutions.stream().map(ExecutionEntity::getId).collect(Collectors.toSet()); - //Build the subProcess hierarchy + // Build the subProcess hierarchy for (SubProcess subProcess : subProcessesToCreate.values()) { if (!processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(subProcess.getId())) { ExecutionEntity embeddedSubProcess = createEmbeddedSubProcessHierarchy(subProcess, defaultContinueParentExecution, subProcessesToCreate, movingExecutionIds, processInstanceChangeState, commandContext); @@ -670,20 +720,27 @@ protected List createEmbeddedSubProcessAndExecutions(Collection } } - //Adds the execution (leaf) to the subProcess + // Adds the execution (leaf) to the subProcess List newChildExecutions = new ArrayList<>(); for (FlowElementMoveEntry flowElementMoveEntry : moveToFlowElements) { FlowElement newFlowElement = flowElementMoveEntry.getNewFlowElement(); ExecutionEntity parentExecution; if (newFlowElement.getSubProcess() != null && processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(newFlowElement.getSubProcess().getId())) { parentExecution = processInstanceChangeState.getCreatedEmbeddedSubProcesses().get(newFlowElement.getSubProcess().getId()); + + } else if ((newFlowElement instanceof Task || newFlowElement instanceof CallActivity) && isFlowElementMultiInstance(newFlowElement) && !movingExecutions.get(0).isMultiInstanceRoot() && + processInstanceChangeState.getCreatedMultiInstanceRootExecution().containsKey(newFlowElement.getId())) { + + parentExecution = processInstanceChangeState.getCreatedMultiInstanceRootExecution().get(newFlowElement.getId()); + } else { parentExecution = defaultContinueParentExecution; } if (isEventSubProcessStart(newFlowElement)) { - //EventSubProcessStarts are created later if the eventSubProcess was not created already during another move + // EventSubProcessStarts are created later if the eventSubProcess was not created already during another move processInstanceChangeState.addPendingEventSubProcessStartEvent((StartEvent) newFlowElement, parentExecution); + } else { ExecutionEntity newChildExecution; if (moveExecutionEntityContainer.isDirectExecutionMigration() && isDirectFlowElementExecutionMigration(flowElementMoveEntry.originalFlowElement, flowElementMoveEntry.newFlowElement)) { @@ -696,6 +753,16 @@ protected List createEmbeddedSubProcessAndExecutions(Collection if (newChildExecution != null) { + if (moveExecutionEntityContainer.getFlowElementLocalVariableMap().containsKey(newFlowElement.getId())) { + newChildExecution.setVariablesLocal(moveExecutionEntityContainer.getFlowElementLocalVariableMap().get(newFlowElement.getId())); + } + + if (movingExecutions.get(0).isMultiInstanceRoot() && isFlowElementMultiInstance(newFlowElement) && hasSameMultiInstanceConfig(movingExecutions.get(0).getCurrentFlowElement(), newFlowElement)) { + newChildExecution.setMultiInstanceRoot(true); + newChildExecution.setActive(false); + processInstanceChangeState.addCreatedMultiInstanceRootExecution(newFlowElement.getId(), newChildExecution); + } + if (newFlowElement instanceof UserTask && !moveExecutionEntityContainer.hasNewExecutionId(newChildExecution.getId())) { @@ -710,17 +777,20 @@ protected List createEmbeddedSubProcessAndExecutions(Collection } if (newFlowElement instanceof CallActivity && !moveExecutionEntityContainer.isDirectExecutionMigration()) { - processEngineConfiguration.getActivityInstanceEntityManager().recordActivityStart(newChildExecution); - - FlowableEventDispatcher eventDispatcher = processEngineConfiguration.getEventDispatcher(); - if (eventDispatcher != null && eventDispatcher.isEnabled()) { - eventDispatcher.dispatchEvent( - FlowableEventBuilder.createActivityEvent(FlowableEngineEventType.ACTIVITY_STARTED, newFlowElement.getId(), newFlowElement.getName(), newChildExecution.getId(), - newChildExecution.getProcessInstanceId(), newChildExecution.getProcessDefinitionId(), newFlowElement), - processEngineConfiguration.getEngineCfgKey()); + + if (!newChildExecution.isMultiInstanceRoot()) { + processEngineConfiguration.getActivityInstanceEntityManager().recordActivityStart(newChildExecution); + + FlowableEventDispatcher eventDispatcher = processEngineConfiguration.getEventDispatcher(); + if (eventDispatcher != null && eventDispatcher.isEnabled()) { + eventDispatcher.dispatchEvent( + FlowableEventBuilder.createActivityEvent(FlowableEngineEventType.ACTIVITY_STARTED, newFlowElement.getId(), newFlowElement.getName(), newChildExecution.getId(), + newChildExecution.getProcessInstanceId(), newChildExecution.getProcessDefinitionId(), newFlowElement), + processEngineConfiguration.getEngineCfgKey()); + } } - //start boundary events of new call activity + // start boundary events of new call activity CallActivity callActivity = (CallActivity) newFlowElement; List boundaryEvents = callActivity.getBoundaryEvents(); if (CollectionUtil.isNotEmpty(boundaryEvents)) { @@ -1159,6 +1229,36 @@ protected boolean isFlowElementMultiInstance(FlowElement flowElement) { } return false; } + + protected boolean hasSameMultiInstanceConfig(FlowElement sourceElement, FlowElement targetElement) { + MultiInstanceLoopCharacteristics sourceMIConfig = null; + if (sourceElement instanceof Activity) { + sourceMIConfig = ((Activity) sourceElement).getLoopCharacteristics(); + } + + MultiInstanceLoopCharacteristics targetMIConfig = null; + if (targetElement instanceof Activity) { + targetMIConfig = ((Activity) targetElement).getLoopCharacteristics(); + } + + if (sourceMIConfig == null || targetMIConfig == null) { + return false; + } + + if (sourceMIConfig.isSequential() != targetMIConfig.isSequential()) { + return false; + } + + if (sourceMIConfig.getLoopCardinality() != null && !sourceMIConfig.getLoopCardinality().equals(targetMIConfig.getLoopCardinality())) { + return false; + } + + if (targetMIConfig.getLoopCardinality() != null && !targetMIConfig.getLoopCardinality().equals(sourceMIConfig.getLoopCardinality())) { + return false; + } + + return true; + } protected void processCreatedEventSubProcess(EventSubProcess eventSubProcess, ExecutionEntity eventSubProcessExecution, Set movingExecutionIds, CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java index ef3561b3bed..89755179e09 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java @@ -43,6 +43,7 @@ public class MoveExecutionEntityContainer { protected String newOwnerId; protected Map continueParentExecutionMap = new HashMap<>(); protected Map moveToFlowElementMap = new LinkedHashMap<>(); + protected Map> flowElementLocalVariableMap = new HashMap<>(); protected List newExecutionIds = new ArrayList<>(); public MoveExecutionEntityContainer(List executions, List moveToActivityIds) { @@ -206,6 +207,18 @@ public void addNewExecutionId(String executionId) { this.newExecutionIds.add(executionId); } + public Map> getFlowElementLocalVariableMap() { + return flowElementLocalVariableMap; + } + + public void setFlowElementLocalVariableMap(Map> flowElementLocalVariableMap) { + this.flowElementLocalVariableMap = flowElementLocalVariableMap; + } + + public void addLocalVariableMap(String activityId, Map localVariables) { + this.flowElementLocalVariableMap.put(activityId, localVariables); + } + public static class FlowElementMoveEntry { protected FlowElement originalFlowElement; diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/ProcessInstanceChangeState.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/ProcessInstanceChangeState.java index 205281fba6f..44d5aee8d99 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/ProcessInstanceChangeState.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/ProcessInstanceChangeState.java @@ -34,6 +34,7 @@ public class ProcessInstanceChangeState { protected Map> processInstanceActiveEmbeddedExecutions; protected List moveExecutionEntityContainers; protected HashMap createdEmbeddedSubProcess = new HashMap<>(); + protected HashMap createdMultiInstanceRootExecution = new HashMap<>(); protected HashMap pendingEventSubProcessesStartEvents = new HashMap<>(); public ProcessInstanceChangeState() { @@ -100,6 +101,18 @@ public void addCreatedEmbeddedSubProcess(String key, ExecutionEntity executionEn this.createdEmbeddedSubProcess.put(key, executionEntity); } + public HashMap getCreatedMultiInstanceRootExecution() { + return createdMultiInstanceRootExecution; + } + + public void setCreatedMultiInstanceRootExecution(HashMap createdMultiInstanceRootExecution) { + this.createdMultiInstanceRootExecution = createdMultiInstanceRootExecution; + } + + public void addCreatedMultiInstanceRootExecution(String key, ExecutionEntity executionEntity) { + this.createdMultiInstanceRootExecution.put(key, executionEntity); + } + public Map> getProcessInstanceActiveEmbeddedExecutions() { return processInstanceActiveEmbeddedExecutions; } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/entity/ExecutionEntityImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/entity/ExecutionEntityImpl.java index 7d39fc48613..9106d299767 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/entity/ExecutionEntityImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/entity/ExecutionEntityImpl.java @@ -1048,7 +1048,7 @@ public boolean isEnded() { return isEnded; } - public boolean setIsEnded() { + public boolean getIsEnded() { return isEnded; } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationMultiInstanceTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationMultiInstanceTest.java index 111716a41d8..dc9e2d320f8 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationMultiInstanceTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationMultiInstanceTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.tuple; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -34,12 +35,15 @@ import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.job.api.Job; import org.flowable.task.api.Task; import org.flowable.task.api.history.HistoricTaskInstance; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * @author Dennis Federico */ @@ -1217,22 +1221,760 @@ public void testMigrateParallelMultiInstanceSubProcessToSimpleActivity() { assertProcessEnded(processInstance.getId()); } + + @Test + public void testMigrateParallelMultiInstanceTaskWithLocalVariables() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + // MI subProcess root execution, actual subProcess and 1 task + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("task"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(startProcessDefinition.getId()); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("kermit"); + List loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + runtimeService.setVariableLocal(execution.getId(), "localVar", "test"); + taskService.setAssignee(miTask.getId(), "othervalue"); + } + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("othervalue"); + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(3); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks).hasSize(2); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("task"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(migrateToProcessDefinition.getId()); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("kermit"); + loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + assertThat(runtimeService.getVariableLocal(execution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "task", "task"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "task", "task", "task", "task"); + } + + // Complete the process + completeProcessInstanceTasks(processInstance.getId()); + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "task", "task", "afterMultiInstance"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "task", "task", "task", "task", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); + } + @Test - @Disabled("Not supported - Cannot migrate to an arbitrary activity inside an MI subProcess") - public void testMigrateSimpleActivityToActivityInsideMultiInstanceSubProcess() { + public void testMigrateParallelMultiInstanceTaskWithLocalVariablesWithMigrationMapping() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + // MI subProcess root execution, actual subProcess and 1 task + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("task"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(startProcessDefinition.getId()); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("kermit"); + List loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + runtimeService.setVariableLocal(execution.getId(), "localVar", "test"); + taskService.setAssignee(miTask.getId(), "othervalue"); + } + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("othervalue"); + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()) + .addActivityMigrationMapping(ActivityMigrationMapping.createMappingFor("task", "task")); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(3); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks).hasSize(2); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("task"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(migrateToProcessDefinition.getId()); + assertThat(tasks) + .extracting(Task::getAssignee) + .containsOnly("kermit"); + loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + assertThat(runtimeService.getVariableLocal(execution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "task", "task"); + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "task", "task", "task", "task"); + } + + // Complete the process + completeProcessInstanceTasks(processInstance.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "task", "task", "afterMultiInstance"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "task", "task", "task", "task", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); } + + @Test + public void testMigrateParallelMultiInstanceSubProcessWithLocalVariables() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + // MI subProcess root execution, actual subProcess and 1 task + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subTask1", "subTask1"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("subTask1"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(startProcessDefinition.getId()); + List loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + runtimeService.setVariableLocal(execution.getId(), "localVar", "test"); + } + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(5); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subTask1", "subTask1"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks).hasSize(2); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("subTask1"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(migrateToProcessDefinition.getId()); + loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + assertThat(runtimeService.getVariableLocal(execution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1"); + } + + // Complete the process + completeProcessInstanceTasks(processInstance.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1", "afterMultiInstance"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); + } + @Test - @Disabled("Not supported - Cannot migrate to an arbitrary activity inside an MI subProcess") - public void testMigrateSimpleActivityToMultiInstanceSubProcessNestedInsideMultiInstanceSubProcess() { + public void testMigrateParallelMultiInstanceSubProcessWithLocalVariablesWithMigrationMapping() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + // MI subProcess root execution, actual subProcess and 1 task + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subTask1", "subTask1"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("subTask1"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(startProcessDefinition.getId()); + List loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + runtimeService.setVariableLocal(execution.getId(), "localVar", "test"); + } + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()) + .addActivityMigrationMapping(ActivityMigrationMapping.createMappingFor("subTask1", "subTask1")); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(5); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subTask1", "subTask1"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks).hasSize(2); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey) + .containsOnly("subTask1"); + assertThat(tasks) + .extracting(Task::getProcessDefinitionId) + .containsOnly(migrateToProcessDefinition.getId()); + loopCounters = tasks.stream().map(aTask -> taskService.getVariable(aTask.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(0, 1); + + for (Task miTask : tasks) { + Execution execution = runtimeService.createExecutionQuery().executionId(miTask.getExecutionId()).singleResult(); + assertThat(runtimeService.getVariableLocal(execution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1"); + } + + // Complete the process + completeProcessInstanceTasks(processInstance.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1", "afterMultiInstance"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "subTask1", "subTask1", "subTask1", "subTask1", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); } + + @Test + public void testMigrateParallelMultiInstanceServiceTaskWithLocalVariables() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1); + + for (Execution miExecution : executions) { + runtimeService.setVariableLocal(miExecution.getId(), "localVar", "test"); + } + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(3); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("task", "task", "task"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1); + + + for (Execution miExecution : executions) { + assertThat(runtimeService.getVariableLocal(miExecution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance"); + } + + List jobs = managementService.createJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(jobs).hasSize(2); + for (Job job : jobs) { + managementService.executeJob(job.getId()); + } + + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).isNotNull(); + assertThat(task.getTaskDefinitionKey()).isEqualTo("afterMultiInstance"); + + taskService.complete(task.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "afterMultiInstance"); + checkActivityInstances(migrateToProcessDefinition, processInstance, "serviceTask", "task", "task"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "afterMultiInstance"); + } + assertProcessEnded(processInstance.getId()); + } + @Test - @Disabled("Not supported - Cannot migrate to an arbitrary activity inside an MI subProcess") - public void testMigrateMultiInstanceSubProcessActivityToNestedMultiInstanceSubProcessActivity() { + public void testMigrateParallelMultiInstanceSubProcessWithServiceTaskAndLocalVariables() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subtask", "subtask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1, 0, 1); + + for (Execution miExecution : executions) { + runtimeService.setVariableLocal(miExecution.getId(), "localVar", "test"); + } + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(5); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("parallelMISubProcess", "parallelMISubProcess", "parallelMISubProcess", "subtask", "subtask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1, 0, 1); + + + for (Execution miExecution : executions) { + assertThat(runtimeService.getVariableLocal(miExecution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance"); + } + + List jobs = managementService.createJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(jobs).hasSize(2); + for (Job job : jobs) { + managementService.executeJob(job.getId()); + } + + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).isNotNull(); + assertThat(task.getTaskDefinitionKey()).isEqualTo("afterMultiInstance"); + + taskService.complete(task.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "afterMultiInstance"); + checkActivityInstances(migrateToProcessDefinition, processInstance, "serviceTask", "subtask", "subtask"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateParallelMultiInstanceCallActivityWithLocalVariables() { + ProcessDefinition startProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml"); + deployProcessDefinition("my deploy", "org/flowable/engine/test/api/oneTaskProcess.bpmn20.xml"); + + // Start the processInstance + Map variableMap = new HashMap<>(); + ArrayNode collectionArray = processEngineConfiguration.getObjectMapper().createArrayNode(); + ObjectNode element1Node = collectionArray.addObject(); + element1Node.put("name", "John Doe"); + element1Node.put("age", 30); + ObjectNode element2Node = collectionArray.addObject(); + element2Node.put("name", "Jane Doe"); + element2Node.put("age", 29); + variableMap.put("myCollection", collectionArray); + ProcessInstance processInstance = runtimeService.startProcessInstanceById(startProcessDefinition.getId(), variableMap); + + // Progress to the MI subProcess + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertThat(task).extracting(Task::getTaskDefinitionKey).isEqualTo("beforeMultiInstance"); + completeTask(task); + + // Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("callActivity", "callActivity", "callActivity"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(startProcessDefinition.getId()); + Execution miRoot = executions.stream().filter(e -> ((ExecutionEntity) e).isMultiInstanceRoot()).findFirst().get(); + Map miRootVars = runtimeService.getVariables(miRoot.getId()); + assertThat(miRootVars) + .extracting("nrOfActiveInstances", "nrOfCompletedInstances") + .containsExactly(2, 0); + List loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1); + + for (Execution miExecution : executions) { + runtimeService.setVariableLocal(miExecution.getId(), "localVar", "test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(startProcessDefinition, processInstance, "userTask", "beforeMultiInstance"); + checkActivityInstances(startProcessDefinition, processInstance, "callActivity", "callActivity", "callActivity"); + } + + ProcessDefinition migrateToProcessDefinition = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml"); + + // Prepare and action the migration + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(migrateToProcessDefinition.getId()); + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions).hasSize(3); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactly("callActivity", "callActivity", "callActivity"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(migrateToProcessDefinition.getId()); + loopCounters = executions.stream().map(anExecution -> runtimeService.getVariable(anExecution.getId(), "loopCounter", Integer.class)) + .collect(Collectors.toList()); + assertThat(loopCounters).containsExactlyInAnyOrder(null, 0, 1); + + + for (Execution miExecution : executions) { + assertThat(runtimeService.getVariableLocal(miExecution.getId(), "localVar")).isEqualTo("test"); + } + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance"); + checkActivityInstances(migrateToProcessDefinition, processInstance, "callActivity", "callActivity", "callActivity", "callActivity", "callActivity"); + } + + List subProcessInstances = runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstance.getId()).list(); + assertThat(subProcessInstances).hasSize(2); + for (ProcessInstance subProcessInstance : subProcessInstances) { + completeProcessInstanceTasks(subProcessInstance.getId()); + } + + completeProcessInstanceTasks(processInstance.getId()); + + if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { + checkActivityInstances(migrateToProcessDefinition, processInstance, "userTask", "beforeMultiInstance", "afterMultiInstance"); + checkActivityInstances(migrateToProcessDefinition, processInstance, "callActivity", "callActivity", "callActivity", "callActivity", "callActivity"); + + checkTaskInstance(migrateToProcessDefinition, processInstance, "beforeMultiInstance", "afterMultiInstance"); + } + + assertProcessEnded(processInstance.getId()); } @Test diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationTest.java index f0096cb6dd4..d919b1e1406 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationTest.java @@ -1441,7 +1441,7 @@ public void testSimpleMigrationWithMultiInstanceTask() { .contains("parallelTasks"); if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { - checkActivityInstances(procDefTwoTasks, processInstance, "userTask", "beforeMultiInstance", "parallelTasks", "parallelTasks", "parallelTasks", "parallelTasks"); + checkActivityInstances(procDefTwoTasks, processInstance, "userTask", "beforeMultiInstance", "parallelTasks", "parallelTasks"); checkTaskInstance(procDefTwoTasks, processInstance, "beforeMultiInstance", "parallelTasks", "parallelTasks", "parallelTasks", "parallelTasks"); } @@ -1449,7 +1449,7 @@ public void testSimpleMigrationWithMultiInstanceTask() { completeProcessInstanceTasks(processInstance.getId()); if (HistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, processEngineConfiguration)) { - checkActivityInstances(procDefTwoTasks, processInstance, "userTask", "beforeMultiInstance", "parallelTasks", "parallelTasks", "parallelTasks", "parallelTasks"); + checkActivityInstances(procDefTwoTasks, processInstance, "userTask", "beforeMultiInstance", "parallelTasks", "parallelTasks"); checkTaskInstance(procDefTwoTasks, processInstance, "beforeMultiInstance", "parallelTasks", "parallelTasks", "parallelTasks", "parallelTasks"); } diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml new file mode 100644 index 00000000000..64935fa981c --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-callactivity-local-variable.bpmn20.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml new file mode 100644 index 00000000000..e46c71a9ccb --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-servicetask.bpmn20.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-callactivity.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-callactivity.bpmn20.xml new file mode 100644 index 00000000000..ad3c59ffa48 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-callactivity.bpmn20.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml new file mode 100644 index 00000000000..7e32bedcd62 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-local-variable.bpmn20.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml new file mode 100644 index 00000000000..5855879d1c8 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-subprocess-servicetask.bpmn20.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml new file mode 100644 index 00000000000..b8a05099916 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/parallel-multi-instance-task-local-variable.bpmn20.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file