martinweiler opened a new issue, #7162:
URL: https://github.com/apache/incubator-kie/issues/7162

   ### Describe the bug
   
   In multi-threaded/asynchronous execution scenarios (such as parallel 
branches with asynchronous tasks converging on a gateway), multiple threads can 
attempt to load and mutate the same `ProcessInstance` concurrently. 
   
   Previously, in-memory process instance locks were released immediately in 
the execution `finally` block before the surrounding database transaction had 
committed. This created a race condition:
   1. **Thread A** updates the process instance state in the DB and releases 
its in-memory lock before transaction commit.
   2. **Thread B** acquires the lock before Thread A's transaction commits and 
reads a stale state/version from the database.
   3. **Thread B** later attempts to persist changes with an outdated version, 
triggering a `ProcessInstanceOptimisticLockingException` (or unnecessary 
retries/rollbacks).
   
   ---
   
   ### **High-Level Technical Implementation**
   
   1. **Transaction-Aware Lock Strategy 
(`TransactionAwareProcessInstanceLockStrategy`)**
      - Introduced a lock strategy decorator that defers the release of the 
in-memory `ProcessInstance` lock until **after the surrounding transaction 
commits** (`afterCompletion` / `afterCommit`).
      - Read operations continue releasing locks immediately in `finally` 
blocks, while write operations synchronize unlock callbacks with the 
transaction lifecycle.
   
   2. **Framework Integration (Quarkus & Spring Boot)**
      - **Quarkus:** Registered synchronization callbacks using 
`TransactionSynchronizationRegistry` in `JDBCProcessInstancesFactory`.
      - **Spring Boot:** Integrated transaction synchronization and updated job 
timeout interceptors to use `PROPAGATION_REQUIRES_NEW`.
   
   3. **Stale Snapshot Eviction on Lock Entry**
      - Updated `AbstractProcessInstance.executeInWorkflowProcessInstance` to 
discard any cached process instance snapshot loaded prior to lock acquisition, 
ensuring reload queries occur inside the lock.
   
   4. **Integration Testing & Verification**
      - Added an asynchronous parallel-split test (`asyncsplit.bpmn` and 
`OptimisticLockingTest`) verifying that parallel branches execute cleanly 
without optimistic locking conflicts or repeated execution attempts.
   
   
   ### Expected behavior
   
   When executing parallel asynchronous branches that update the same process 
instance (e.g., converging on a parallel join gateway), each branch should 
execute its tasks exactly once. The in-memory lock on the process instance 
should remain held until the surrounding database transaction has fully 
committed so that subsequent threads always read the updated state/version and 
complete without `ProcessInstanceOptimisticLockingException` or transaction 
retries.
   
   
   
   ### Actual behavior
   
   The in-memory lock on the `ProcessInstance` was released immediately in the 
execution `finally` block before the database transaction committed. When 
parallel worker threads completed near-simultaneously, the second thread 
acquired the lock while the first thread's transaction was still in flight, 
reading a stale snapshot and optimistic-locking version from the database. 
Consequently, the second thread's write attempt failed with a 
`ProcessInstanceOptimisticLockingException`, causing a transaction rollback and 
job retry (resulting in tasks/work items executing multiple times).
   
   
   
   ### How to Reproduce?
   
   1. Enable optimistic locking with JDBC persistence 
(`kogito.persistence.optimistic.lock=true`).
   2. Use the embedded job service with JPA persistence
   3. Define a BPMN process with an asynchronous parallel split 
(`<parallelGateway>`) spawning two concurrent asynchronous tasks 
(`customAsync="true"`) that converge at a parallel join (`<parallelGateway>`).
   4. Inside each task handler, simulate concurrent completion with a brief 
wait time (e.g., 100 ms) and record execution attempts using a 
non-transactional counter outside the DB.
   5. Start a new instance of the process via REST (`POST /<process-name>`).
   6. Await process instance completion.
   7. Observe the execution attempt counter: prior to the fix, the counter 
exceeds the expected count (e.g., 3+ instead of 2) and logs show 
`ProcessInstanceOptimisticLockingException` during concurrent join evaluation.
   
   ### Output of `uname -a` or `ver`
   
   _No response_
   
   ### Output of `java -version`
   
   _No response_
   
   ### GraalVM version (if different from Java)
   
   _No response_
   
   ### Kogito version or git rev (or at least Quarkus version if you are using 
Kogito via Quarkus platform BOM)
   
   _No response_
   
   ### Build tool (ie. output of `mvnw --version` or `gradlew --version`)
   
   _No response_
   
   ### Additional information
   
   _No response_


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to