Behaviour inclusive gateway in Unittest vs. JobExecutor

I was developing a Unittest (JobExcecutor disabled) for a process that is forked and joined with an inclusive gateway at some point. The joining gateway is followed by an asynchronous service task.
At this point my test was failing because there were two jobs discovered for the same activityId by:
managementService().createJobQuery().activityId(activityId).active().singleResult();

I didn’t expect that so I did a little investigation and found out that there were also two (concurrent) executions present for the same activityId.

When i ran the process within my application (having the JobExcutor activated) the tasks were executed just once (as expected).
So my question is: Does the JobExcecutor handle these cases where there are two jobs for the same activityId ? Or is it just wrong that there are two jobs/executions present for the same task/activityId after a join (that’s what i would expect) ?
As a side note: I had parallel gateways in the same places where I have the inclusive gateways now, before. The behaviour was different here. After the join there was only one job/execution present.

Hi,

Can you please share the unit test?

Cheers,
Thorben

Probably not, I’m sorry :sweat:. I am working for a customer on that.

Can you reduce it to a minimal process + test case that reproduces the behavior? Taking your description, your scenario is not clear to me and a unit test would very much help with that.

Cheers,
Thorben

I’ll try that. Thanks for helping.

Regards
Jan

The unit testing template should help you get going.

Ah, thanks. Didn’t know about that one.

I tried to reproduce it with our own test infrastructure quickly first but unfortunately I wasn’t able to.
But maybe it provides enough context to understand my problem, so i attatched the BPMN, Unittest and the Delegate.

Like in the actual process I am testing I have some async tasks executed in two inclusive paths. In the test of the actual process (other than in this test) i have two pending jobs for the next service task. So here it would be two jobs for the activityId “Task3”, hence the query for a single result fails.

Basically I use the same engine services here as I do in my other test. So guess it must have something to do either with the other process or something i do differently in the test (which i am not aware of, yet). Anyway I guess I have to dig a little further myself.

Here’s a screenshot of the crucial part of the other process. Maybe you see something suspicious there at first glance. If not, thanks a lot again for your help anyway.

Jan


About the problem: In the test I get two jobs and executions for the task “Abrechnung Vorgang schreiben” when querying for jobs for this activity id via the engine services.


Here’s the BPMN:
ProcessWithParallelGateway.bpmn (7.3 KB)

Here’s my Test:


import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import org.camunda.bpm.engine.ManagementService;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.repository.DeploymentBuilder;
import org.camunda.bpm.engine.runtime.Job;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class InclusiveGatewayProzessTest {
    
    @Configuration
    @Import(value={ProzessTestConfig.class})
    static class Config{
        @Bean
        public InclusiveGatewayProcessDelegate inclusiveGatewayProcessDelegate(){
            return new InclusiveGatewayProcessDelegate();
        }
    }
    
    @Autowired
    private ProcessEngine engine;

    
    
    @Before
    public void setUp() {

        deploy("ProcessWithParallelGateway.bpmn");

    }
    
    
    @Test
    public void test(){
        engine.getRuntimeService().startProcessInstanceByKey("ProcessWithParallelGateway");
        executeAsyncTask("Task1");
        executeAsyncTask("Task2");
        executeAsyncTask("Task3");
        
        int runningInstances = engine.getRuntimeService().createProcessInstanceQuery().processDefinitionKey("ProcessWithParallelGateway").list().size();
        assertThat(runningInstances, is(0));
    }
    
    
    private void executeAsyncTask(String activityId){
        ManagementService managementService = engine.getManagementService();
        Job job = managementService.createJobQuery().active().activityId(activityId).singleResult();
        managementService.executeJob(job.getId());
    }
    
    private void deploy(String... resources) {
        
        
        DeploymentBuilder deploymentBuilder = engine.getRepositoryService().createDeployment();
        for (String resource: resources) {
            deploymentBuilder.addClasspathResource(resource);
        }
        deploymentBuilder.deploy();
    }

}

The Delegate just sysouts the activityId


import org.camunda.bpm.engine.delegate.DelegateExecution;

public class InclusiveGatewayProcessDelegate {
    
    public void executeTask(DelegateExecution execution){
        System.out.println("Executed Task: "+ execution.getActivityInstanceId());
    }

}

Hi @JHoelter,

Thanks for the code and the details.
If the inclusive gateway fires for the first time when there are still tokens upstream that it should wait for, then that would be a bug in the process engine.

The logic that decides when the gateway fires is not so trivial and has proven to be the source of bugs in the past. Such bugs often show up depending on the process models used (for example confer https://app.camunda.com/jira/browse/CAM-4960), so I would look into that direction.

To further debug this, you could try the following:

  • Does your failing test also fail with the latest Camunda version (7.5.0)? Also, which version do you use?
  • In which state is the process instance when the gateway triggers the first time? You could determine that by adding an execution listener on the join’s end event. In that listener, call the method RuntimeService#getActivityInstance and print its result.

Cheers,
Thorben

Thanks @thorben it sounds like the bug is releated as i also have mutliple scopes due to severeal call activites in that case, right ? We are actually still on 7.4 so 7.5 might be worth a try. As this might take me some time because I have to setup the tomcat first (unfortuntely we haven’t migrated to spring boot, yet), i’ll try that later.
So in the meantime I inspected the engine service method you suggested in a debugging session and found that it has two outgoing transitions to the task behind the joining gateway.

    ├── transition to/from abrechnungVorgangSchreiben:177
    └── transition to/from abrechnungVorgangSchreiben:178
[TransitionInstanceImpl[executionId=177, targetActivityId=abrechnungVorgangSchreiben, activityName=Abrechnung Vorgang schreiben, activityType=serviceTask, id=177, parentActivityInstanceId=44, processInstanceId=44, processDefinitionId=Rueckkauf:1:3], 

TransitionInstanceImpl[executionId=178, targetActivityId=abrechnungVorgangSchreiben, activityName=Abrechnung Vorgang schreiben, activityType=serviceTask, id=178, parentActivityInstanceId=44, processInstanceId=44, processDefinitionId=Rueckkauf:1:3]]

This seems to be incorrect, right ?

I ran the test against camunda 7.5 and unfortunately observed the same behaviour in my test.

What still confuses me the most is that running the process in our regular app shows a differet (and correct) behaviour.
We’re using a ProcessEnginePlugin that adds a listener to any tasks “start” event and logs that it was accessed (via BPMNParseListener). I also kept the suggested logging for the crucial task. What i can see is that anything following the gateway is clearly executed twice:

07-22 14:12:10,724 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Abrechnung Vorgang schreiben
07-22 14:12:10,729 INFO  RueckkaufProzess:156 Activity: └── Rueckkauf:1:3=>44
    ├── transition to/from abrechnungVorgangSchreiben:177
    └── abrechnungVorgangSchreiben=>abrechnungVorgangSchreiben:258

07-22 14:12:10,730 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Finanzamtmeldung durchführen
07-22 14:12:10,742 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Abrechnung Vorgang schreiben
07-22 14:12:10,743 INFO  RueckkaufProzess:156 Activity: └── Rueckkauf:1:3=>44
    ├── finanzamtsmeldung=>finanzamtsmeldung:260
    └── abrechnungVorgangSchreiben=>abrechnungVorgangSchreiben:296

07-22 14:12:10,744 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Finanzamtmeldung durchführen

When I run the process in the application it is executed just once. The main difference i am aware of is that the test is not using the automatic execution by the JobExcecutor but is done “manually”. Do you think that could be the source of the error. Maybe I am doing something wrong with how I execute the Jobs (see test example above) ?

07-22 14:38:44,399 INFO  jobexecutor:579 ENGINE-14008 Adding new exclusive job to job executor context. Job Id='1818'
07-22 14:38:44,401 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Abrechnung Vorgang schreiben
07-22 14:38:44,426 INFO  RueckkaufProzess:156 Activity: └── Rueckkauf:3:1381=>1405
    ├── transition to/from vertragsversionRueckkaufAnlegen:1688
    └── abrechnungVorgangSchreiben=>abrechnungVorgangSchreiben:1822

07-22 14:38:44,427 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Finanzamtmeldung durchführen
07-22 14:38:44,429 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = MeldungFinanzamt Pruefung(Meldung_Finanzamt.bpmn) - Task = Finanzamtmeldung Relevanz prüfen
07-22 14:38:44,929 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = MeldungFinanzamt Pruefung(Meldung_Finanzamt.bpmn) - Task = null
07-22 14:38:44,932 INFO  ProzessTaskLogger:30 Prozess Test - Aktuelle Aktion: Prozess = Rückkauf(Rueckkauf.bpmn) - Task = Erfolg

Hi Jan,

I think crucial to finding the root cause is that we identify the state the process instance is in when the gateway triggers (incorrectly) for the first time. That means, which activities/transitions(aka jobs) are active at the time when the gateway triggers? I assume that this state is different in your unit test compared to job execution.

To get to the root cause, I propose the following steps:

  1. Identify the process instance state when the gateway fires for the first time. You can do that by reading the outputs of your logger or by looking into the tables ACT_HI_ACTINST and ACT_HI_JOBLOG and comparing execution times.
  2. Reproduce that exact state in a unit test (flexible process instantiation should be useful here)
  3. Assuming that this reproduces the bug, we can now actually debug the problem and probably reduce it to a focused process model

Cheers,
Thorben

Hi Torben,

i had some new findings. My observation that the process shows a different behaviour having the job executor acitvated was wrong.
It just appeared to be that way because the last terminate end event (Erfolg) destroyed all existing tokens before the inclusive gateway is triggered a second time. I just overlooked the fact that some of the tasks in one of the parallel paths were not triggered. I was able to see that when querying the activity instance history.

Given that i wasn’t able to reproduce this behaviour with my simple process I had a new suspicion of what causes the problem in my process: boths paths can lead to an “abortion” of the process by terminate end events. So I tested what happens when all of those transitions from both paths are joined in the inclusive gateway instead of ending in a terminate end event. This indeed led to a working unittest which suggests that my suspicion might be correct.

Right now I am trying to reproduce that in the simple process but had no luck so far. There has to be at least some second dependency besides the “path ending in terminate end event”.

Regards,
Jan

Hi @thorben,

i finally was able to reproduce the behaviour in a simple process model. I assume that the bug is correlated to having a (boundary) timer event in one of the paths.
I’ll attach the bpmn and my modified test. Will that be ok for you or do you need the test within the unit test template you sent me ? I’m asking because I am already way out schedule for the actual feature I have to deliver and will probably need some more time to figure out how to work around this issue :sweat_smile:.

I sysout some infos - Activity Instance History ordered by start time descending, Pending Jobs, Current Activity Tree - in the test that illustrate the behaviour:

---------------- Activity History ----------------
HistoricActivityInstanceEntity[activityId=InclusiveGateway_0z98nld, activityName=null, activityType=inclusiveGateway, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=0, startTime=Tue Jul 26 00:00:00 CEST 2016, endTime=Tue Jul 26 00:00:00 CEST 2016, eventType=null, executionId=16, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=timerEvent, activityName=null, activityType=boundaryTimer, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=0, startTime=Tue Jul 26 00:00:00 CEST 2016, endTime=Tue Jul 26 00:00:00 CEST 2016, eventType=null, executionId=16, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=Task2, activityName=Servicetask 2, activityType=serviceTask, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=0, startTime=Tue Jul 26 00:00:00 CEST 2016, endTime=Tue Jul 26 00:00:00 CEST 2016, eventType=null, executionId=16, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=InclusiveGateway_0z98nld, activityName=null, activityType=inclusiveGateway, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=0, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=15, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=endEvent_4a511870-4843-4885-b606-41cacf1a7c2a, activityName=Ende - Dummy, activityType=noneEndEvent, activityInstanceId=null, activityInstanceState=1, parentActivityInstanceId=19, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=1, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=19, processDefinitionId=dummyProcess:1:9, processInstanceId=19, tenantId=null]
HistoricActivityInstanceEntity[activityId=dummyDelegateTask, activityName=Dummy Delegate, activityType=serviceTask, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=19, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=3, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=19, processDefinitionId=dummyProcess:1:9, processInstanceId=19, tenantId=null]
HistoricActivityInstanceEntity[activityId=userTask1, activityName=Usertask 1, activityType=userTask, activityInstanceId=null, activityInstanceState=2, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=31, taskAssignee=null, durationInMillis=32555132, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Tue Jul 26 00:00:00 CEST 2016, eventType=null, executionId=28, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=ExclusiveGateway_1ub6nng, activityName=null, activityType=exclusiveGateway, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=1, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=16, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=userTask1, activityName=Usertask 1, activityType=userTask, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=25, taskAssignee=null, durationInMillis=63, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=22, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=startEvent_b4e0469b-414e-4d8e-b041-1db19f1b7c12, activityName=Start + Dummy, activityType=startEvent, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=19, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=0, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=19, processDefinitionId=dummyProcess:1:9, processInstanceId=19, tenantId=null]
HistoricActivityInstanceEntity[activityId=subprocess1, activityName=Subprocess1, activityType=callActivity, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=19, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=104, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=17, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=InclusiveGateway_0k66q7u, activityName=null, activityType=inclusiveGateway, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=2, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=11, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
HistoricActivityInstanceEntity[activityId=StartEvent_1, activityName=null, activityType=startEvent, activityInstanceId=null, activityInstanceState=0, parentActivityInstanceId=11, calledProcessInstanceId=null, calledCaseInstanceId=null, taskId=null, taskAssignee=null, durationInMillis=2, startTime=Mon Jul 25 14:57:24 CEST 2016, endTime=Mon Jul 25 14:57:24 CEST 2016, eventType=null, executionId=11, processDefinitionId=ProcessWithParallelGateway:1:3, processInstanceId=11, tenantId=null]
---------------- Pending Jobs ----------------
MessageEntity[repeatnull, id=35, revision=1, duedate=null, lockOwner=null, lockExpirationTime=null, executionId=15, processInstanceId=11, isExclusive=true, retries=3, jobHandlerType=async-continuation, jobHandlerConfiguration=transition-create-scope, exceptionByteArray=null, exceptionByteArrayId=null, exceptionMessage=null, deploymentId=1]
MessageEntity[repeatnull, id=40, revision=1, duedate=null, lockOwner=null, lockExpirationTime=null, executionId=16, processInstanceId=11, isExclusive=true, retries=3, jobHandlerType=async-continuation, jobHandlerConfiguration=transition-create-scope, exceptionByteArray=null, exceptionByteArrayId=null, exceptionMessage=null, deploymentId=1]
----------------------------------------------
---------------- Activity Instance Tree ----------------
└── ProcessWithParallelGateway:1:3=>11
    ├── transition to/from Task3:15
    └── transition to/from Task3:16

When I remove the boundary timer event the gateway is just triggered once


ProcessWithParallelGateway.bpmn (13.4 KB)

package de.vorsorge.prozess.test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.TemporalUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.camunda.bpm.engine.ManagementService;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.camunda.bpm.engine.repository.Deployment;
import org.camunda.bpm.engine.repository.DeploymentBuilder;
import org.camunda.bpm.engine.runtime.Execution;
import org.camunda.bpm.engine.runtime.Job;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

import com.google.common.collect.Maps;

import de.vorsorge.theo.prozess.model.Prozess;
import de.vorsorge.theo.util.Tag;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class InclusiveGatewayProzessTest {
    
    private static final String DUMMY_DELEGATE_TASK_ID = "dummyDelegateTask";
    private static final String TASK3_ACTIVITY_ID = "Task3";
    private static final String TASK2_ACTIVITY_ID = "Task2";

    @Configuration
    @Import(value={ProzessTestConfig.class})
    static class Config{
        @Bean
        public InclusiveGatewayProcessDelegate inclusiveGatewayProcessDelegate(){
            return new InclusiveGatewayProcessDelegate();
        }
    }
    
    @Autowired
    private ProcessEngine engine;

    
    
    @Before
    public void setUp() {

        deploy("ProcessWithParallelGateway.bpmn");
        mockSubProzess("dummyProcess", DummyDelegate.class);

    }
    
    
    @Test
    public void test(){
        HashMap<String, Object> variables = Maps.newHashMap();
        variables.put("timerDate", tomorrow());
        ProcessInstance processInstance = engine.getRuntimeService().startProcessInstanceByKey("ProcessWithParallelGateway", variables);
        submitUserTask1();
        executeAsyncTask(DUMMY_DELEGATE_TASK_ID);
        triggerTimerEvent();
        executeAsyncTask(TASK2_ACTIVITY_ID);
        
        printActivityHistory();
        
        printActiveJobs();
        
        printActivityInstanceTree(processInstance);
        
        executeAsyncTask(TASK3_ACTIVITY_ID);
        
        int runningInstances = engine.getRuntimeService().createProcessInstanceQuery().processDefinitionKey("ProcessWithParallelGateway").list().size();
        assertThat(runningInstances, is(0));
        assertThat(activityInstanceCount(TASK3_ACTIVITY_ID), is(1));
    }


    public void printActivityInstanceTree(ProcessInstance processInstance) {
        System.out.println("---------------- Activity Instance Tree ----------------");
        System.out.println(engine.getRuntimeService().getActivityInstance(processInstance.getId()));
        System.out.println("----------------------------------------------------");
    }


    public void printActiveJobs() {
        System.out.println("---------------- Pending Jobs ----------------");
        engine.getManagementService().createJobQuery().active().list()
            .stream()
            .forEach(job -> System.out.println(job.toString()));
        System.out.println("----------------------------------------------");
    }


    public void printActivityHistory() {
        System.out.println("---------------- Activity History ----------------");
        engine.getHistoryService().createHistoricActivityInstanceQuery().orderByHistoricActivityInstanceStartTime().desc().list()
            .stream()
            .forEach(activityInstance -> System.out.println(activityInstance.toString()));
        System.out.println("--------------------------------------------------");
    }


    private void triggerTimerEvent() {
        ClockUtil.setCurrentTime(tomorrow());
        executeAsyncTask("timerEvent");
    }


    private Date tomorrow() {
        return Date.from(LocalDate.now().plusDays(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
    }


    private void submitUserTask1() {
        HashMap<String, Object> variables = Maps.newHashMap();
        variables.put("abbrechen", false);
        engine.getFormService().submitTaskForm(engine.getTaskService().createTaskQuery().executionId(executionIdFor("userTask1")).singleResult().getId(), variables);
    }


    private String executionIdFor(String activityId) {
        String executionId = engine.getRuntimeService().createExecutionQuery().activityId(activityId).singleResult().getId();
        return executionId;
    }
    
    private int activityInstanceCount(String activityId){
        return engine.getHistoryService().createHistoricActivityInstanceQuery().activityId(activityId).list().size();
    }
    
    
    private void executeAsyncTask(String activityId){
        ManagementService managementService = engine.getManagementService();
        List<Job> jobs = managementService.createJobQuery().active().activityId(activityId).list();
        assertThat(jobs.size(), is(1));
        
        managementService.executeJob(jobs.get(0).getId());
    }
    
    private void deploy(String... resources) {
        
        
        DeploymentBuilder deploymentBuilder = engine.getRepositoryService().createDeployment();
        for (String resource: resources) {
            deploymentBuilder.addClasspathResource(resource);
        }
        deploymentBuilder.deploy();
    }
    
    public void mockSubProzess(String key, Class<?> dummyDelegate) {
        
        
        String name = "Dummy";
        BpmnModelInstance modelInstance = Bpmn
                .createExecutableProcess(key).name(name)
                .startEvent().name("Start + "+name).serviceTask().id(DUMMY_DELEGATE_TASK_ID)
                .name("Dummy Delegate").camundaAsyncBefore()
                .camundaClass(dummyDelegate.getName()).endEvent()
                .name("Ende - "+name).done();
        
        deployMockProzess(key, modelInstance);
    }

    public Deployment deployMockProzess(String key, BpmnModelInstance modelInstance) {
        
        /* 
         * Workaround for https://app.camunda.com/jira/browse/CAM-4787
         */
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        Bpmn.writeModelToStream(output , modelInstance);
        return engine.getRepositoryService().createDeployment()
                .addInputStream(key + ".bpmn", new ByteArrayInputStream(output.toByteArray())).deploy();
    }
    
    public static final class DummyDelegate implements JavaDelegate{

        @Override
        public void execute(DelegateExecution execution) throws Exception {
            System.out.println(this.getClass().getSimpleName());
            
        }
        
    }

}

HI Jan,

Thanks for the process model. The problem is the sequence flow from Usertask1 to Servicetask 2 via a boundary event. The current implementation of the inclusive gateway does not detect a token at Usertask1 (or any activity before that) to be able to reach it. I am also not sure if this is intended by the specification. The relevant part is

I don’t know if taking a boundary event can be considered a “directed path formed by sequence flow”. As usual, the spec does not define such a term precisely :see_no_evil:

Anyway, if you just add a regular sequence flow from Usertask 1 to Servicetask2 with sequence flow condition set to ${false}, the gateway should behave correctly.

Cheers,
Thorben

Hi @thorben,

that did the trick. Thanks a lot for helping.

I have one more thing in this regard. It is working now but the model is somewhat misleading because now I have a transition that actually is never used. We use this kind of “pattern” (usertask + boundary timer event) a lot in our processes to express/model the same thing: During the process there is some kind of delay like a “time for reply”. In this period some responsible person can abort the process for different reasons (e.g. cancellation by the customer), but the only permitted way to go on with the process is by reducing the delay time (changing the date) which technically changes the result of the timers expression.
Is there any way to model this kind of behaviour differently (better) ? Or is this case something you would consider to support in later versions of camunda ? At least in case you decide to classify it as “directed path formed by sequence flow”.

Regards
Jan

Hi @JHoelter,

Given that we interpret the spec such that it should work with boundary events, then yes, we could raise a bug ticket and fix it some time.

Specificiation-wise, I think your use case fits better into CMMN than BPMN. In CMMN, this kind of control flow should be easier to express with sentries.

I personally consider using inclusive gateways extensively an anti-pattern. It makes the models and in particular when a gateway triggers hard to understand. So my recommendation is to rather avoid inclusive gateways than change the timer boundary events.

Cheers,
Thorben

Thanks for your assesment. I’ll think about it. In this special case the two existing paths are mandatory and an additional path (missing in the attached screenshot) is optional. So the semantics of an inclusive gateway fits nicely here. Using a parallel gateway for the paths and then using an exclusive gateway to skip the optional path conditionally would also work but imho this also seems weired as the path should not have been entered in the first place.
But I get your point about inclusive gateways being hard to understand.

Allowing two outgoing default flows for inclusive gateways would be nice and very expressive for my case ;).

Anyway, thanks again.

Regards
Jan