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]