Asynchronous Service Invocation using Flowable

In Flowable documentation:

"When the external service wants to trigger the waiting process instance again, this can be done synchronously and asynchronously. To prevent optimistic lock exceptions, the asynchronous trigger would be the best solution. By default an async job is exclusive, which means that the process instance will be locked and it’s guaranteed that no other activity on the process instance can interfere with the trigger logic.”

But we are faced with the fact that the behavior is different from the documentation.

Here is the example with simplified test: https://github.com/sgolikov/flowable

Test with external callback, when replayback return faster then ServiceTask finish execution:

  • ServiceTaskInvoker - emulate call to external system and sleep for 1000 ms.

In BPMN scheme we add ServiceTask:

<serviceTask id="serviceInvocation" flowable:delegateExpression="${serviceTaskInvoker}" flowable:triggerable="true" flowable:async="true" flowable:exclusive="true"/>

Step1 [main-thread]

Start process;

Step2 [AsyncJobExecutor-thread-1] [JOB_1]

Begin to execute ServiceTask.

Step3 [main-thread]

While ServiceTask sleeping in [AsyncJobExecutor-thread-1], in [main-thread] we triggerAsync(execution.getId()).

And there is a problem (as we see here flowable-engine change parameters for JOB_1 => JOB_COUNT and REV_):

[main-thread] update ACT_RU_EXECUTION SET REV_ = ?, JOB_COUNT_ = ? where ID_ = ? and REV_ = ?

Step4 [AsyncJobExecutor-thread-2] [JOB_2]

In new thread flowable engine try to lock processInstance (updateProcessInstanceLockTime):

Preparing: update ACT_RU_EXECUTION set LOCK_TIME_ = ? where ID_ = ? and (LOCK_TIME_ is null OR LOCK_TIME_ < ?)

BUT Updates: 0

Step5 [AsyncJobExecutor-thread-1] [JOB_1]

ServiceTask begin to execute after sleep, and try to update execution:
update ACT_RU_EXECUTION SET REV_ = ?, JOB_COUNT_ = ? where ID_ = ? and REV_ = ?

BUT it was already updated earlier in Step3-4.

As a result, we have: FlowableOptimisticLockingException.

I agree that this documentation could be better. The way to avoid this race condition is to place your call to the external system in a transaction listener:

Context.getTransactionContext().addTransactionListener(TransactionState.COMMITTED,
    commandContext -> {
        someService.someMethod();
    });

This way everything is committed when the call is made and the service task is ready to be triggered.

A better description is in this video at 38:29:

1 Like

Thanks a lot, wwitt!
At first glance it really helps) But we’ll check it in details a bit later.

Thank’s!
We checked it in details and this is really a solution!