RuntimeService.setVariable ignores async boundaries?

We are running some unit test using Camunda 7.8.6 against the H2 in memory database to verify our External Task implementation works correctly and the behaviour we are observing is surprising with respect to our current understanding of Transactional Boundaries in Camunda.

In particular, our code is correctly polling the task, and executing

runtimeService.setVariable(task.getExecutionId,"myVariable","myValue")

but we forgot to invoke externalTaskService.complete(task, workerId)

At the end of the unit test we verify if the process has myVariable set, and it is actually set to the right value, which is not what we were expecting. If the task hasn’t been completed, why would the variable be committed ? In our understanding, process variables changes are committed transactionally when the next transactional boundary is reached

I think i need to see your Unit test and process model to be sure but i think you may be misunderstanding transactions, external tasks and how the testing procedure is intended to work. But i can probably give you suggestions if you post up an example of what you’re trying to test and what you’re achieved so far.

I have prepared an example that shows what I am talking about:

import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineConfiguration;
import org.camunda.bpm.engine.externaltask.LockedExternalTask;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class ExternalTaskBehaviour {


public static final String TOPIC_NAME = "org.edmondo1984.example";

public static void main(String[] args) throws InterruptedException {
    ProcessEngine processEngine = ProcessEngineConfiguration
            .createStandaloneInMemProcessEngineConfiguration().buildProcessEngine();
    BpmnModelInstance modelInstance = Bpmn.createExecutableProcess("repro")
            .name("Transaction and External Tasks?")
            .startEvent()
            .name("Start event")
            .serviceTask()
            .name("Assign Approver")
            .camundaType("External")
            .camundaTopic(TOPIC_NAME)
            .endEvent()
            .name("End process")
            .done();

    // deploy process model
    processEngine.getRepositoryService().createDeployment().addModelInstance("repro.bpmn", modelInstance).deploy();
    // start consumer
    AtomicBoolean terminateLock = new AtomicBoolean(false);
    Thread thread = buildThread(processEngine,terminateLock);
    thread.start();
    // start process model
    ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey("repro");
    Thread.sleep(1000);
    System.out.println("Process variable " + processEngine.getRuntimeService().getVariable(processInstance.getId(),"variable-1"));
    terminateLock.set(true);
    processEngine.close();

}

public static Thread buildThread(ProcessEngine processEngine, AtomicBoolean lock){
    return new Thread(() -> {
        try {
            if(!lock.get()){
                Thread.sleep(100);
                List<LockedExternalTask> externalTasks = processEngine.getExternalTaskService().fetchAndLock(1, "worker-id-1").topic(TOPIC_NAME, 10000).execute();
                System.out.println("Found " + externalTasks.size() + " tasks, processing");
                if(externalTasks.size() >0 ){
                    LockedExternalTask externalTask = externalTasks.get(0);
                    processEngine.getRuntimeService().setVariable(externalTask.getExecutionId(),"variable-1","value-1");
                }
            }
            else{
                System.out.println("End lock set, terminating");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    });


}

}

and I made it available as a gist here :https://gist.github.com/edmondo1984/ee60692b1c0d6fcaaf6ce1df7f1fd94c . What is surprising is that line 42 will log a process variable which has been set asynchronously without any transaction commit (the process engine has not reached the next transaction boundary because externalTaskServce.complete() has not been called by the async thread

I think you should maybe consider testing using the Camunda Asserts Lib that will make things a lot easier for your testing.

This issue with your test is that when you’re setting that variable by using the process engine object to set it directly it really has nothing to do with the external task. if you want to complete an external task with variables you need complete the task.

	  Map<String,Object> vars = new HashMap<String, Object>();
	  vars.put("SomeVarName", "SomeVarValue");
	  rule.getExternalTaskService().complete("someTaskID", "WorkerID", vars);

Thanks @Niall for the tips.

What is surprising is that this behaviour is undocumented: according to the documentation “Transaction in Processes”

On any such external trigger (i.e., start a process, complete a task, signal an execution), the engine runtime will advance in the process until it reaches wait states on each active path of execution. A wait state is a task which is performed later , which means that the engine persists the current execution to the database and waits to be triggered again.

It is not mentioned that using runtimeService.setVariable(executionId,variableName,variableValue) will automatically trigger the persistence of the execution on the database. I think it is worth providing a chapter about it in that part of the documentation, or maybe even wonder if one wants to deprecate setVariable on the RuntimeService at all.

In general I think this is a needed improvement in the documentation of the open-source project, but I will raise the ticket to the customer service for paid ones as well.

1 Like