flowable / flowable-engine

A compact and highly efficient workflow and Business Process Management (BPM) platform for developers, system admins and business users.
https://www.flowable.org
Apache License 2.0
7.97k stars 2.62k forks source link

The concurrency issue when using DbSqlSession to refresh CacheEntity to the database. #3833

Closed JohnnieWalkerGoldLabelReserve closed 9 months ago

JohnnieWalkerGoldLabelReserve commented 9 months ago

Process Image

DX-20240201@2x

Problem description

As shown in the flow chart above, I have created two simultaneous boundary message events in the ShareTask, with one having cancel_activity set to true and the other set to false. When I synchronously publish the two events using two threads, concurrency issues arise.

new Thread(() ->{
    runtimeService.messageEventReceived(
        "CancelActivity",
        execution.getId(),
        params);
}).start();

new Thread(() ->{
    runtimeService.messageEventReceived(
        "NotCancelActivity",
        execution.getId(),
        params);
}).start(); 

1.The NotCancelActivity thread ends its process, calls the method to close the context, and begins to flush the DbSqlSession's Entity changes to the database. This includes updating the related activities in the ACT_RU_EXECUTION table to an ended status (normal completion), and incrementing the corresponding version number by 1.

2.Before the flush operation completes, the CancelActivity thread also finishes processing and starts to close the context. At this point, the CancelActivity thread must mark the NotCancelActivity-related entity as ended (abnormally canceled) due to the cancel_activity option, and increment the version number by 1.

3.NotCancelActivity first flushes the entity changes to the database, completing the REV update. Subsequently, CancelActivity reads the same entity and attempts to update it. Since the version number has changed from 1 to 2, the condition REV=1 is no longer met, resulting in an exception.

version Flowable 7.0.1 with SpringBoot

filiphr commented 9 months ago

This works as designed.

What you have explained is valid optimistic lock that makes sure that Flowable stays in a consistent state. There is nothing wrong in the DbSqlSession since it is never been accessed concurrently from different threads.

The reason why you are seeing the optimistic locking exception is due to the fact that the second step is going to actually try and delete the "Share Task", this will trigger a deletion of the execution for the "NotCancelActivity" execution. However, in step 1 the "NotCancelActivity" will actually be deleted since it will be executed.

When the execution happens in the same time both threads are going to read REV_=1 for the "NotCancelActivity" execution. Depending on which thread finishes first there would either be an optimistic lock exception on the first or the second step.

If Step 1 finishes first, then in Step 2 when trying to delete "NotCancelActivity" it will try to delete the execution with the appropriate id and revision 1. However, that execution no longer exists and thus an optimistic lock exception is thrown.

If Step 2 finishes first, then in Steps when trying to delete the "Share Task" and its associated "NotCancelActivity" execution, a similar thing happens. There is no execution for "NotCancelActivity" anymore and thus there is an optimistic lock exception.

Since this works as designed we are going to close this. However, feel free what you think should happen and how an optimistic locking exception can be avoided here.