diff --git a/engine-rest/engine-rest-openapi/src/main/templates/models/org/camunda/bpm/engine/rest/dto/ModificationDto.ftl b/engine-rest/engine-rest-openapi/src/main/templates/models/org/camunda/bpm/engine/rest/dto/ModificationDto.ftl index c9ae64b7ec3..1b662c794c4 100644 --- a/engine-rest/engine-rest-openapi/src/main/templates/models/org/camunda/bpm/engine/rest/dto/ModificationDto.ftl +++ b/engine-rest/engine-rest-openapi/src/main/templates/models/org/camunda/bpm/engine/rest/dto/ModificationDto.ftl @@ -37,6 +37,14 @@ desc = "A process instance query." /> + <@lib.property + name = "historicProcessInstanceQuery" + type = "ref" + dto = "HistoricProcessInstanceQueryDto" + desc = "A historic process instance query. It is advised to include the `unfinished` filter in the + historic process instance query as finished instances cause failures for the modification." + /> + <@lib.property name = "instructions" type = "array" diff --git a/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/dto/ModificationDto.java b/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/dto/ModificationDto.java index c10d204617d..9822b044c65 100644 --- a/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/dto/ModificationDto.java +++ b/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/dto/ModificationDto.java @@ -19,6 +19,8 @@ import java.util.List; import org.camunda.bpm.engine.ProcessEngine; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; +import org.camunda.bpm.engine.rest.dto.history.HistoricProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.dto.runtime.ProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.dto.runtime.modification.ProcessInstanceModificationInstructionDto; import org.camunda.bpm.engine.runtime.ModificationBuilder; @@ -30,6 +32,7 @@ public class ModificationDto { protected List instructions; protected List processInstanceIds; protected ProcessInstanceQueryDto processInstanceQuery; + protected HistoricProcessInstanceQueryDto historicProcessInstanceQuery; protected String processDefinitionId; protected boolean skipIoMappings; protected boolean skipCustomListeners; @@ -99,4 +102,11 @@ public void setAnnotation(String annotation) { this.annotation = annotation; } + public HistoricProcessInstanceQueryDto getHistoricProcessInstanceQuery() { + return historicProcessInstanceQuery; + } + + public void setHistoricProcessInstanceQuery(HistoricProcessInstanceQueryDto historicProcessInstanceQuery) { + this.historicProcessInstanceQuery = historicProcessInstanceQuery; + } } diff --git a/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/impl/ModificationRestServiceImpl.java b/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/impl/ModificationRestServiceImpl.java index 8ea839b70e3..e7cca71ea06 100644 --- a/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/impl/ModificationRestServiceImpl.java +++ b/engine-rest/engine-rest/src/main/java/org/camunda/bpm/engine/rest/impl/ModificationRestServiceImpl.java @@ -22,9 +22,11 @@ import org.camunda.bpm.engine.BadUserRequestException; import org.camunda.bpm.engine.batch.Batch; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; import org.camunda.bpm.engine.rest.ModificationRestService; import org.camunda.bpm.engine.rest.dto.ModificationDto; import org.camunda.bpm.engine.rest.dto.batch.BatchDto; +import org.camunda.bpm.engine.rest.dto.history.HistoricProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.dto.runtime.ProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.exception.InvalidRequestException; import org.camunda.bpm.engine.runtime.ModificationBuilder; @@ -74,6 +76,12 @@ private ModificationBuilder createModificationBuilder(ModificationDto dto) { builder.processInstanceQuery(processInstanceQuery); } + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = dto.getHistoricProcessInstanceQuery(); + if(historicProcessInstanceQueryDto != null) { + HistoricProcessInstanceQuery historicProcessInstanceQuery = historicProcessInstanceQueryDto.toQuery(getProcessEngine()); + builder.historicProcessInstanceQuery(historicProcessInstanceQuery); + } + if (dto.isSkipCustomListeners()) { builder.skipCustomListeners(); } diff --git a/engine-rest/engine-rest/src/test/java/org/camunda/bpm/engine/rest/ModificationRestServiceInteractionTest.java b/engine-rest/engine-rest/src/test/java/org/camunda/bpm/engine/rest/ModificationRestServiceInteractionTest.java index 19438ad947b..757cc2cb6be 100644 --- a/engine-rest/engine-rest/src/test/java/org/camunda/bpm/engine/rest/ModificationRestServiceInteractionTest.java +++ b/engine-rest/engine-rest/src/test/java/org/camunda/bpm/engine/rest/ModificationRestServiceInteractionTest.java @@ -32,9 +32,12 @@ import org.camunda.bpm.engine.AuthorizationException; import org.camunda.bpm.engine.BadUserRequestException; +import org.camunda.bpm.engine.HistoryService; import org.camunda.bpm.engine.RuntimeService; import org.camunda.bpm.engine.batch.Batch; +import org.camunda.bpm.engine.impl.HistoricProcessInstanceQueryImpl; import org.camunda.bpm.engine.impl.ProcessInstanceQueryImpl; +import org.camunda.bpm.engine.rest.dto.history.HistoricProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.dto.runtime.ProcessInstanceQueryDto; import org.camunda.bpm.engine.rest.exception.InvalidRequestException; import org.camunda.bpm.engine.rest.util.ModificationInstructionBuilder; @@ -57,6 +60,7 @@ public class ModificationRestServiceInteractionTest extends AbstractRestServiceT protected static final String EXECUTE_MODIFICATION_ASYNC_URL = PROCESS_INSTANCE_URL + "/executeAsync"; protected RuntimeService runtimeServiceMock; + protected HistoryService historyServiceMock; protected ModificationBuilder modificationBuilderMock; @Before @@ -64,6 +68,9 @@ public void setUpRuntimeData() { runtimeServiceMock = mock(RuntimeService.class); when(processEngine.getRuntimeService()).thenReturn(runtimeServiceMock); + historyServiceMock = mock(HistoryService.class); + when(processEngine.getHistoryService()).thenReturn(historyServiceMock); + modificationBuilderMock = mock(ModificationBuilder.class); when(modificationBuilderMock.cancelAllForActivity(any())).thenReturn(modificationBuilderMock); when(modificationBuilderMock.startAfterActivity(any())).thenReturn(modificationBuilderMock); @@ -299,6 +306,39 @@ public void executeModificationWithValidProcessInstanceQuerySync() { verify(modificationBuilderMock).execute(); } + @Test + public void executeModificationWithValidHistoricProcessInstanceQuerySync() { + + when(historyServiceMock.createHistoricProcessInstanceQuery()).thenReturn(new HistoricProcessInstanceQueryImpl()); + Map json = new HashMap(); + + List> instructions = new ArrayList>(); + instructions.add(ModificationInstructionBuilder.startAfter().activityId("activityId").getJson()); + json.put("processDefinitionId", "processDefinitionId"); + + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = new HistoricProcessInstanceQueryDto(); + historicProcessInstanceQueryDto.setProcessInstanceBusinessKey("foo"); + + json.put("historicProcessInstanceQuery", historicProcessInstanceQueryDto); + + json.put("instructions", instructions); + + given() + .contentType(ContentType.JSON) + .body(json) + .then() + .expect() + .statusCode(Status.NO_CONTENT.getStatusCode()) + .when() + .post(EXECUTE_MODIFICATION_SYNC_URL); + + verify(historyServiceMock, times(1)).createHistoricProcessInstanceQuery(); + verify(modificationBuilderMock).startAfterActivity("activityId"); + verify(modificationBuilderMock).historicProcessInstanceQuery(historicProcessInstanceQueryDto.toQuery(processEngine)); + verify(modificationBuilderMock).execute(); + } + + @Test public void executeModificationWithValidProcessInstanceQueryAsync() { @@ -330,6 +370,74 @@ public void executeModificationWithValidProcessInstanceQueryAsync() { verify(modificationBuilderMock).executeAsync(); } + @Test + public void executeModificationWithValidHistoricProcessInstanceQueryAsync() { + when(historyServiceMock.createHistoricProcessInstanceQuery()).thenReturn(new HistoricProcessInstanceQueryImpl()); + Map json = new HashMap(); + + List> instructions = new ArrayList>(); + instructions.add(ModificationInstructionBuilder.startAfter().activityId("activityId").getJson()); + + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = new HistoricProcessInstanceQueryDto(); + historicProcessInstanceQueryDto.setProcessInstanceBusinessKey("foo"); + + json.put("historicProcessInstanceQuery", historicProcessInstanceQueryDto); + json.put("instructions", instructions); + json.put("processDefinitionId", "processDefinitionId"); + + given() + .contentType(ContentType.JSON) + .body(json) + .then() + .expect() + .statusCode(Status.OK.getStatusCode()) + .when() + .post(EXECUTE_MODIFICATION_ASYNC_URL); + + verify(historyServiceMock, times(1)).createHistoricProcessInstanceQuery(); + verify(modificationBuilderMock).startAfterActivity("activityId"); + verify(modificationBuilderMock).historicProcessInstanceQuery(historicProcessInstanceQueryDto.toQuery(processEngine)); + verify(modificationBuilderMock).executeAsync(); + } + + @Test + public void executeModificationWithBothProcessInstanceQueries() { + when(historyServiceMock.createHistoricProcessInstanceQuery()).thenReturn(new HistoricProcessInstanceQueryImpl()); + when(runtimeServiceMock.createProcessInstanceQuery()).thenReturn(new ProcessInstanceQueryImpl()); + + Map json = new HashMap(); + + List> instructions = new ArrayList>(); + instructions.add(ModificationInstructionBuilder.startAfter().activityId("activityId").getJson()); + + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = new HistoricProcessInstanceQueryDto(); + historicProcessInstanceQueryDto.setProcessInstanceBusinessKey("foo"); + + ProcessInstanceQueryDto processInstanceQueryDto = new ProcessInstanceQueryDto(); + processInstanceQueryDto.setBusinessKey("foo"); + + json.put("processInstanceQuery", processInstanceQueryDto); + json.put("historicProcessInstanceQuery", historicProcessInstanceQueryDto); + json.put("instructions", instructions); + json.put("processDefinitionId", "processDefinitionId"); + + given() + .contentType(ContentType.JSON) + .body(json) + .then() + .expect() + .statusCode(Status.OK.getStatusCode()) + .when() + .post(EXECUTE_MODIFICATION_ASYNC_URL); + + verify(historyServiceMock, times(1)).createHistoricProcessInstanceQuery(); + verify(modificationBuilderMock).startAfterActivity("activityId"); + verify(modificationBuilderMock).historicProcessInstanceQuery(historicProcessInstanceQueryDto.toQuery(processEngine)); + verify(runtimeServiceMock, times(1)).createProcessInstanceQuery(); + verify(modificationBuilderMock).processInstanceQuery(processInstanceQueryDto.toQuery(processEngine)); + verify(modificationBuilderMock).executeAsync(); + } + @Test public void executeModificationWithInvalidProcessInstanceQuerySync() { @@ -359,6 +467,35 @@ public void executeModificationWithInvalidProcessInstanceQuerySync() { } + @Test + public void executeModificationWithInvalidHistoricProcessInstanceQuerySync() { + when(historyServiceMock.createHistoricProcessInstanceQuery()).thenReturn(new HistoricProcessInstanceQueryImpl()); + + Map json = new HashMap(); + + String message = "Process instance ids is null"; + doThrow(new BadUserRequestException(message)).when(modificationBuilderMock).execute(); + + List> instructions = new ArrayList>(); + instructions.add(ModificationInstructionBuilder.startAfter().activityId("acivityId").getJson()); + + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = new HistoricProcessInstanceQueryDto(); + historicProcessInstanceQueryDto.setProcessInstanceBusinessKey("foo"); + + json.put("historicProcessInstanceQuery", historicProcessInstanceQueryDto); + json.put("instructions", instructions); + json.put("processDefinitionId", "processDefinitionId"); + + given() + .contentType(ContentType.JSON) + .body(json) + .then() + .expect() + .statusCode(Status.BAD_REQUEST.getStatusCode()) + .when() + .post(EXECUTE_MODIFICATION_SYNC_URL); + } + @Test public void executeModificationWithInvalidProcessInstanceQueryAsync() { @@ -384,6 +521,31 @@ public void executeModificationWithInvalidProcessInstanceQueryAsync() { .post(EXECUTE_MODIFICATION_ASYNC_URL); } + @Test + public void executeModificationWithInvalidHistoricProcessInstanceQueryAsync() { + when(historyServiceMock.createHistoricProcessInstanceQuery()).thenReturn(new HistoricProcessInstanceQueryImpl()); + Map json = new HashMap(); + + List> instructions = new ArrayList>(); + instructions.add(ModificationInstructionBuilder.startAfter().activityId("acivityId").getJson()); + + HistoricProcessInstanceQueryDto historicProcessInstanceQueryDto = new HistoricProcessInstanceQueryDto(); + historicProcessInstanceQueryDto.setProcessInstanceBusinessKey("foo"); + + json.put("historicProcessInstanceQuery", historicProcessInstanceQueryDto); + json.put("instructions", instructions); + json.put("processDefinitionId", "processDefinitionId"); + + given() + .contentType(ContentType.JSON) + .body(json) + .then() + .expect() + .statusCode(Status.OK.getStatusCode()) + .when() + .post(EXECUTE_MODIFICATION_ASYNC_URL); + } + @Test public void executeModificationWithNullInstructionsSync() { doThrow(new BadUserRequestException("Instructions must be set")).when(modificationBuilderMock).execute(); diff --git a/engine/src/main/java/org/camunda/bpm/engine/impl/ModificationBuilderImpl.java b/engine/src/main/java/org/camunda/bpm/engine/impl/ModificationBuilderImpl.java index da29b63610c..e943c4cf088 100644 --- a/engine/src/main/java/org/camunda/bpm/engine/impl/ModificationBuilderImpl.java +++ b/engine/src/main/java/org/camunda/bpm/engine/impl/ModificationBuilderImpl.java @@ -25,6 +25,7 @@ import org.camunda.bpm.engine.batch.Batch; import org.camunda.bpm.engine.exception.NotValidException; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; import org.camunda.bpm.engine.impl.cmd.AbstractProcessInstanceModificationCommand; import org.camunda.bpm.engine.impl.cmd.ActivityAfterInstantiationCmd; import org.camunda.bpm.engine.impl.cmd.ActivityBeforeInstantiationCmd; @@ -40,6 +41,7 @@ public class ModificationBuilderImpl implements ModificationBuilder { protected CommandExecutor commandExecutor; protected ProcessInstanceQuery processInstanceQuery; + protected HistoricProcessInstanceQuery historicProcessInstanceQuery; protected List processInstanceIds; protected List instructions; protected String processDefinitionId; @@ -114,6 +116,12 @@ public ModificationBuilder processInstanceQuery(ProcessInstanceQuery processInst return this; } + @Override + public ModificationBuilder historicProcessInstanceQuery(HistoricProcessInstanceQuery historicProcessInstanceQuery) { + this.historicProcessInstanceQuery = historicProcessInstanceQuery; + return this; + } + @Override public ModificationBuilder skipCustomListeners() { this.skipCustomListeners = true; @@ -153,6 +161,10 @@ public ProcessInstanceQuery getProcessInstanceQuery() { return processInstanceQuery; } + public HistoricProcessInstanceQuery getHistoricProcessInstanceQuery() { + return historicProcessInstanceQuery; + } + public List getProcessInstanceIds() { return processInstanceIds; } diff --git a/engine/src/main/java/org/camunda/bpm/engine/impl/cmd/AbstractModificationCmd.java b/engine/src/main/java/org/camunda/bpm/engine/impl/cmd/AbstractModificationCmd.java index 945aa0d4747..10239dedbbf 100644 --- a/engine/src/main/java/org/camunda/bpm/engine/impl/cmd/AbstractModificationCmd.java +++ b/engine/src/main/java/org/camunda/bpm/engine/impl/cmd/AbstractModificationCmd.java @@ -23,6 +23,7 @@ import java.util.Set; import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.camunda.bpm.engine.impl.HistoricProcessInstanceQueryImpl; import org.camunda.bpm.engine.impl.ModificationBuilderImpl; import org.camunda.bpm.engine.impl.ProcessInstanceQueryImpl; import org.camunda.bpm.engine.impl.interceptor.Command; @@ -53,6 +54,12 @@ protected Collection collectProcessInstanceIds() { collectedProcessInstanceIds.addAll(processInstanceQuery.listIds()); } + final HistoricProcessInstanceQueryImpl historicProcessInstanceQuery = + (HistoricProcessInstanceQueryImpl) builder.getHistoricProcessInstanceQuery(); + if(historicProcessInstanceQuery != null) { + collectedProcessInstanceIds.addAll(historicProcessInstanceQuery.listIds()); + } + return collectedProcessInstanceIds; } diff --git a/engine/src/main/java/org/camunda/bpm/engine/runtime/ModificationBuilder.java b/engine/src/main/java/org/camunda/bpm/engine/runtime/ModificationBuilder.java index dcff4d6ed1a..9da2316ae25 100644 --- a/engine/src/main/java/org/camunda/bpm/engine/runtime/ModificationBuilder.java +++ b/engine/src/main/java/org/camunda/bpm/engine/runtime/ModificationBuilder.java @@ -24,6 +24,7 @@ import org.camunda.bpm.engine.authorization.Permissions; import org.camunda.bpm.engine.authorization.Resources; import org.camunda.bpm.engine.batch.Batch; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; public interface ModificationBuilder extends InstantiationBuilder{ @@ -116,5 +117,12 @@ public interface ModificationBuilder extends InstantiationBuilder */ Batch executeAsync(); + + /** + * @param historicProcessInstanceQuery a query which selects the process instances to modify. + * It is advised to include the `unfinished` filter in the historicProcessInstanceQuery as finished instances cause failures for the modification. + * Query results are restricted to process instances for which the user has {@link Permissions#READ_HISTORY} permission. + */ + ModificationBuilder historicProcessInstanceQuery(HistoricProcessInstanceQuery historicProcessInstanceQuery); } diff --git a/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionAsyncTest.java b/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionAsyncTest.java index 702ff0bbd03..183231c10c6 100644 --- a/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionAsyncTest.java +++ b/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionAsyncTest.java @@ -35,12 +35,15 @@ import java.util.List; import org.assertj.core.api.Assertions; +import org.camunda.bpm.engine.HistoryService; import org.camunda.bpm.engine.ProcessEngineConfiguration; import org.camunda.bpm.engine.ProcessEngineException; import org.camunda.bpm.engine.RuntimeService; +import org.camunda.bpm.engine.TaskService; import org.camunda.bpm.engine.batch.Batch; import org.camunda.bpm.engine.batch.history.HistoricBatch; import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; import org.camunda.bpm.engine.impl.batch.BatchSeedJobHandler; import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity; @@ -55,6 +58,7 @@ import org.camunda.bpm.engine.runtime.ProcessInstanceQuery; import org.camunda.bpm.engine.runtime.VariableInstance; import org.camunda.bpm.engine.task.Task; +import org.camunda.bpm.engine.test.Deployment; import org.camunda.bpm.engine.test.ProcessEngineRule; import org.camunda.bpm.engine.test.RequiredHistoryLevel; import org.camunda.bpm.engine.test.bpmn.multiinstance.DelegateEvent; @@ -86,6 +90,7 @@ public class ModificationExecutionAsyncTest { protected ProcessEngineConfigurationImpl configuration; protected RuntimeService runtimeService; + protected HistoryService historyService; protected BpmnModelInstance instance; @@ -110,6 +115,7 @@ public static Collection scenarios() throws ParseException { @Before public void initServices() { runtimeService = rule.getRuntimeService(); + historyService = rule.getHistoryService(); } @Before @@ -245,6 +251,17 @@ public void testNullProcessInstanceQueryAsync() { } } + @Test + public void testNullHistoricProcessInstanceQueryAsync() { + + try { + runtimeService.createModification("processDefinitionId").startAfterActivity("user1").historicProcessInstanceQuery(null).executeAsync(); + fail("Should not succeed"); + } catch (ProcessEngineException e) { + assertThat(e.getMessage()).contains("Process instance ids is empty"); + } + } + @Test public void createModificationWithNonExistingProcessDefinitionId() { DeploymentWithDefinitions deployment = testRule.deploy(instance); @@ -841,6 +858,114 @@ public void testBatchExecutionFailureWithMissingProcessInstance() { assertThat(failedJob.getExceptionMessage()).contains("Process instance '" + deletedProcessInstanceId + "' cannot be modified"); } + @Test + public void testBatchExecutionFailureWithHistoricQueryThatMatchesDeletedInstance() { + DeploymentWithDefinitions deployment = testRule.deploy(instance); + ProcessDefinition processDefinition = deployment.getDeployedProcessDefinitions().get(0); + + List startedInstances = helper.startInstances("process1", 3); + RuntimeService runtimeService = rule.getRuntimeService(); + + String deletedProcessInstanceId = startedInstances.get(0); + + runtimeService.deleteProcessInstance(deletedProcessInstanceId, "test"); + + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().processDefinitionId(processDefinition.getId()); + + Batch batch = runtimeService + .createModification(processDefinition.getId()) + .startAfterActivity("user1") + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + helper.completeSeedJobs(batch); + + // when + helper.executeJobs(batch); + + // then the remaining process instance was modified + for (String processInstanceId : startedInstances) { + if (processInstanceId.equals(deletedProcessInstanceId)) { + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNull(updatedTree); + continue; + } + + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNotNull(updatedTree); + assertEquals(processInstanceId, updatedTree.getProcessInstanceId()); + + assertThat(updatedTree).hasStructure( + describeActivityInstanceTree( + processDefinition.getId()) + .activity("user1") + .activity("user2") + .done()); + } + + // and one batch job failed and has 2 retries left + List modificationJobs = helper.getExecutionJobs(batch); + assertEquals(1, modificationJobs.size()); + + Job failedJob = modificationJobs.get(0); + assertEquals(2, failedJob.getRetries()); + assertThat(failedJob.getExceptionMessage()).startsWith("ENGINE-13036"); + assertThat(failedJob.getExceptionMessage()).contains("Process instance '" + deletedProcessInstanceId + "' cannot be modified"); + } + + @Test + @Deployment(resources = { "org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml" }) + public void testBatchExecutionWithHistoricQueryUnfinished() { + // given + List startedInstances = helper.startInstances("oneTaskProcess", 3); + + TaskService taskService = rule.getTaskService(); + Task task = taskService.createTaskQuery().processInstanceId(startedInstances.get(0)).singleResult(); + String processDefinitionId = task.getProcessDefinitionId(); + String completedProcessInstanceId = task.getProcessInstanceId(); + assertNotNull(task); + taskService.complete(task.getId()); + + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().unfinished().processDefinitionId(processDefinitionId); + assertEquals(2, historicProcessInstanceQuery.count()); + + // then + Batch batch = runtimeService + .createModification(processDefinitionId) + .startAfterActivity("theStart") + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + helper.completeSeedJobs(batch); + + // when + helper.executeJobs(batch); + + // then the remaining process instance was modified + for (String processInstanceId : startedInstances) { + if (processInstanceId.equals(completedProcessInstanceId)) { + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNull(updatedTree); + continue; + } + + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNotNull(updatedTree); + assertEquals(processInstanceId, updatedTree.getProcessInstanceId()); + + assertThat(updatedTree).hasStructure( + describeActivityInstanceTree( + processDefinitionId) + .activity("theTask") + .activity("theTask") + .done()); + } + + // and one batch job failed and has 2 retries left + List modificationJobs = helper.getExecutionJobs(batch); + assertEquals(0, modificationJobs.size()); + } + @Test public void testBatchCreationWithProcessInstanceQuery() { int processInstanceCount = 15; @@ -862,6 +987,142 @@ public void testBatchCreationWithProcessInstanceQuery() { assertBatchCreated(batch, processInstanceCount); } + @Test + public void testBatchCreationWithHistoricProcessInstanceQuery() { + int processInstanceCount = 15; + DeploymentWithDefinitions deployment = testRule.deploy(instance); + ProcessDefinition processDefinition = deployment.getDeployedProcessDefinitions().get(0); + helper.startInstances("process1", 15); + + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().processDefinitionId(processDefinition.getId()); + assertEquals(processInstanceCount, historicProcessInstanceQuery.count()); + + // when + Batch batch = runtimeService + .createModification(processDefinition.getId()) + .startAfterActivity("user1") + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + // then a batch is created + assertBatchCreated(batch, processInstanceCount); + } + + @Test + @Deployment(resources = { "org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml" }) + public void testBatchExecutionFailureWithFinishedInstanceId() { + // given + List startedInstances = helper.startInstances("oneTaskProcess", 3); + + TaskService taskService = rule.getTaskService(); + Task task = taskService.createTaskQuery().processInstanceId(startedInstances.get(0)).singleResult(); + String processDefinitionId = task.getProcessDefinitionId(); + String completedProcessInstanceId = task.getProcessInstanceId(); + assertNotNull(task); + taskService.complete(task.getId()); + + // then + Batch batch = runtimeService + .createModification(processDefinitionId) + .startAfterActivity("theStart") + .processInstanceIds(startedInstances) + .executeAsync(); + + helper.completeSeedJobs(batch); + + // when + helper.executeJobs(batch); + + // then the remaining process instance was modified + for (String processInstanceId : startedInstances) { + if (processInstanceId.equals(completedProcessInstanceId)) { + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNull(updatedTree); + continue; + } + + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNotNull(updatedTree); + assertEquals(processInstanceId, updatedTree.getProcessInstanceId()); + + assertThat(updatedTree).hasStructure( + describeActivityInstanceTree( + processDefinitionId) + .activity("theTask") + .activity("theTask") + .done()); + } + + // and one batch job failed and has 2 retries left + List modificationJobs = helper.getExecutionJobs(batch); + assertEquals(1, modificationJobs.size()); + + Job failedJob = modificationJobs.get(0); + assertEquals(2, failedJob.getRetries()); + assertThat(failedJob.getExceptionMessage()).startsWith("ENGINE-13036"); + assertThat(failedJob.getExceptionMessage()).contains("Process instance '" + completedProcessInstanceId + "' cannot be modified"); + } + + + @Test + @Deployment(resources = { "org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml" }) + public void testBatchExecutionFailureWithHistoricQueryThatMatchesFinishedInstance() { + // given + List startedInstances = helper.startInstances("oneTaskProcess", 3); + + TaskService taskService = rule.getTaskService(); + Task task = taskService.createTaskQuery().processInstanceId(startedInstances.get(0)).singleResult(); + String processDefinitionId = task.getProcessDefinitionId(); + String completedProcessInstanceId = task.getProcessInstanceId(); + assertNotNull(task); + taskService.complete(task.getId()); + + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().processDefinitionId(processDefinitionId); + assertEquals(3, historicProcessInstanceQuery.count()); + + // then + Batch batch = runtimeService + .createModification(processDefinitionId) + .startAfterActivity("theStart") + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + helper.completeSeedJobs(batch); + + // when + helper.executeJobs(batch); + + // then the remaining process instance was modified + for (String processInstanceId : startedInstances) { + if (processInstanceId.equals(completedProcessInstanceId)) { + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNull(updatedTree); + continue; + } + + ActivityInstance updatedTree = runtimeService.getActivityInstance(processInstanceId); + assertNotNull(updatedTree); + assertEquals(processInstanceId, updatedTree.getProcessInstanceId()); + + assertThat(updatedTree).hasStructure( + describeActivityInstanceTree( + processDefinitionId) + .activity("theTask") + .activity("theTask") + .done()); + } + + // and one batch job failed and has 2 retries left + List modificationJobs = helper.getExecutionJobs(batch); + assertEquals(1, modificationJobs.size()); + + Job failedJob = modificationJobs.get(0); + assertEquals(2, failedJob.getRetries()); + assertThat(failedJob.getExceptionMessage()).startsWith("ENGINE-13036"); + assertThat(failedJob.getExceptionMessage()).contains("Process instance '" + completedProcessInstanceId + "' cannot be modified"); + } + + @Test public void testBatchCreationWithOverlappingProcessInstanceIdsAndQuery() { int processInstanceCount = 15; @@ -884,6 +1145,53 @@ public void testBatchCreationWithOverlappingProcessInstanceIdsAndQuery() { assertBatchCreated(batch, processInstanceCount); } + @Test + public void testBatchCreationWithOverlappingProcessInstanceIdsAndHistoricQuery() { + int processInstanceCount = 15; + DeploymentWithDefinitions deployment = testRule.deploy(instance); + ProcessDefinition processDefinition = deployment.getDeployedProcessDefinitions().get(0); + List processInstanceIds = helper.startInstances("process1", 15); + + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().processDefinitionId(processDefinition.getId()); + assertEquals(processInstanceCount, historicProcessInstanceQuery.count()); + + // when + Batch batch = runtimeService + .createModification(processDefinition.getId()) + .startTransition("seq") + .processInstanceIds(processInstanceIds) + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + // then a batch is created + assertBatchCreated(batch, processInstanceCount); + } + + @Test + public void testBatchCreationWithOverlappingHistoricQueryAndQuery() { + // given + int processInstanceCount = 15; + DeploymentWithDefinitions deployment = testRule.deploy(instance); + ProcessDefinition processDefinition = deployment.getDeployedProcessDefinitions().get(0); + helper.startInstances("process1", processInstanceCount); + + ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinition.getId()); + assertEquals(processInstanceCount, processInstanceQuery.count()); + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery().processDefinitionId(processDefinition.getId()); + assertEquals(processInstanceCount, historicProcessInstanceQuery.count()); + + // when + Batch batch = runtimeService + .createModification(processDefinition.getId()) + .startTransition("seq") + .processInstanceQuery(processInstanceQuery) + .historicProcessInstanceQuery(historicProcessInstanceQuery) + .executeAsync(); + + // then a batch is created + assertBatchCreated(batch, processInstanceCount); + } + @Test public void testListenerInvocation() { // given diff --git a/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionSyncTest.java b/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionSyncTest.java index 99dd55e7358..0e9e531e710 100644 --- a/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionSyncTest.java +++ b/engine/src/test/java/org/camunda/bpm/engine/test/api/runtime/ModificationExecutionSyncTest.java @@ -29,13 +29,16 @@ import java.util.Collections; import java.util.List; +import org.camunda.bpm.engine.HistoryService; import org.camunda.bpm.engine.ProcessEngineException; import org.camunda.bpm.engine.RuntimeService; +import org.camunda.bpm.engine.history.HistoricProcessInstanceQuery; import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity; import org.camunda.bpm.engine.repository.DeploymentWithDefinitions; import org.camunda.bpm.engine.repository.ProcessDefinition; import org.camunda.bpm.engine.runtime.ActivityInstance; import org.camunda.bpm.engine.runtime.Execution; +import org.camunda.bpm.engine.runtime.ProcessInstanceQuery; import org.camunda.bpm.engine.task.Task; import org.camunda.bpm.engine.test.ProcessEngineRule; import org.camunda.bpm.engine.test.util.ProcessEngineTestRule; @@ -59,6 +62,7 @@ public class ModificationExecutionSyncTest { public RuleChain ruleChain = RuleChain.outerRule(rule).around(testRule); protected RuntimeService runtimeService; + protected HistoryService historyService; protected BpmnModelInstance instance; @Before @@ -76,6 +80,7 @@ public void createBpmnModelInstance() { @Before public void initServices() { runtimeService = rule.getRuntimeService(); + historyService = rule.getHistoryService(); } @After @@ -97,6 +102,78 @@ public void createSimpleModificationPlan() { } } + @Test + public void createSimpleModificationPlanWithHistoricQuery() { + ProcessDefinition processDefinition = testRule.deployAndGetDefinition(instance); + List instances = helper.startInstances("process1", 2); + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery(); + historicProcessInstanceQuery.processDefinitionId(processDefinition.getId()); + + runtimeService.createModification(processDefinition.getId()).startBeforeActivity("user2") + .cancelAllForActivity("user1").historicProcessInstanceQuery(historicProcessInstanceQuery).execute(); + + for (String instanceId : instances) { + List activeActivityIds = runtimeService.getActiveActivityIds(instanceId); + assertEquals(1, activeActivityIds.size()); + assertEquals(activeActivityIds.iterator().next(), "user2"); + } + } + + @Test + public void createSimpleModificationPlanWithIdenticalRuntimeAndHistoryQuery() { + ProcessDefinition processDefinition = testRule.deployAndGetDefinition(instance); + List instances = helper.startInstances("process1", 2); + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery(); + historicProcessInstanceQuery.processDefinitionId(processDefinition.getId()); + ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery(); + processInstanceQuery.processDefinitionId(processDefinition.getId()); + + runtimeService.createModification(processDefinition.getId()).startBeforeActivity("user2") + .cancelAllForActivity("user1").historicProcessInstanceQuery(historicProcessInstanceQuery).processInstanceQuery(processInstanceQuery).execute(); + + for (String instanceId : instances) { + List activeActivityIds = runtimeService.getActiveActivityIds(instanceId); + assertEquals(1, activeActivityIds.size()); + assertEquals(activeActivityIds.iterator().next(), "user2"); + } + } + + @Test + public void createSimpleModificationPlanWithComplementaryRuntimeAndHistoryQueries() { + ProcessDefinition processDefinition = testRule.deployAndGetDefinition(instance); + List instances = helper.startInstances("process1", 2); + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery(); + historicProcessInstanceQuery.processInstanceId(instances.get(0)); + ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery(); + processInstanceQuery.processInstanceId(instances.get(1)); + + runtimeService.createModification(processDefinition.getId()).startBeforeActivity("user2") + .cancelAllForActivity("user1").historicProcessInstanceQuery(historicProcessInstanceQuery).processInstanceQuery(processInstanceQuery).execute(); + + for (String instanceId : instances) { + List activeActivityIds = runtimeService.getActiveActivityIds(instanceId); + assertEquals(1, activeActivityIds.size()); + assertEquals(activeActivityIds.iterator().next(), "user2"); + } + } + + @Test + public void createSimpleModificationPlanWithHistoricQueryUnfinished() { + ProcessDefinition processDefinition = testRule.deployAndGetDefinition(instance); + List instances = helper.startInstances("process1", 2); + HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery(); + historicProcessInstanceQuery.processDefinitionId(processDefinition.getId()).unfinished(); + + runtimeService.deleteProcessInstance(instances.get(0), "test"); + + runtimeService.createModification(processDefinition.getId()).startBeforeActivity("user2") + .cancelAllForActivity("user1").historicProcessInstanceQuery(historicProcessInstanceQuery).execute(); + + List activeActivityIds = runtimeService.getActiveActivityIds(instances.get(1)); + assertEquals(1, activeActivityIds.size()); + assertEquals(activeActivityIds.iterator().next(), "user2"); + } + @Test public void createModificationWithNullProcessInstanceIdsList() { @@ -174,6 +251,16 @@ public void testNullProcessInstanceQuery() { } } + @Test + public void testNullHistoricProcessInstanceQuery() { + try { + runtimeService.createModification("processDefinitionId").startAfterActivity("user1").historicProcessInstanceQuery(null).execute(); + fail("Should not succeed"); + } catch (ProcessEngineException e) { + assertThat(e.getMessage()).contains("Process instance ids is empty"); + } + } + @Test public void createModificationWithNotMatchingProcessDefinitionId() { DeploymentWithDefinitions deployment = testRule.deploy(instance); diff --git a/engine/src/test/resources/org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml b/engine/src/test/resources/org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml new file mode 100644 index 00000000000..8f7ec4955dc --- /dev/null +++ b/engine/src/test/resources/org/camunda/bpm/engine/test/api/runtime/ProcessInstanceModificationTest.syncAfterOneTaskProcess.bpmn20.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +