This is an automated email from the ASF dual-hosted git repository.

martinweiler pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie.git


The following commit(s) were added to refs/heads/main by this push:
     new ff5db7b047c [kie-issues#7162] Delay ProcessInstance lock release to 
transaction commit to avoid stale read in multi-threaded scenarios (#7163)
ff5db7b047c is described below

commit ff5db7b047c7daacd29382939ae2028a210a7bc7
Author: Martin Weiler <[email protected]>
AuthorDate: Fri Oct 9 08:58:59 2026 -0600

    [kie-issues#7162] Delay ProcessInstance lock release to transaction commit 
to avoid stale read in multi-threaded scenarios (#7163)
    
    * [kie-issues#7162] Delay ProcessInstance lock release to transaction 
commit to avoid stale read in multi-threaded scenarios
    
    * Simplify the approach by moving the transaction related code directly 
into ProcessInstanceAtomicLockStrategy
    
    * Restore isReentrant check location
    
    * - make VertxJobScheduler user the vertx platform integration
    
    * - api cleanup
    
    * Align Spring Boot unlock logic with Quarkus using afterCompletion instead 
of afterCommit
    
    * Make Vertx injection optional in SpringbootJobsService
    
    ---------
    
    Co-authored-by: pefernan <[email protected]>
---
 .../persistence/jdbc/JDBCProcessInstances.java     |  29 ++-
 .../app/jobs/quarkus/QuarkusJobsService.java       |   5 +
 .../app/jobs/springboot/SpringbootJobsService.java |   6 +
 .../TransactionJobTimeoutInterceptor.java          |  18 +-
 .../workflow/core/node/AsyncEventNodeInstance.java |   3 +-
 .../process/impl/AbstractProcessInstance.java      |  41 +++-
 .../ContextAwareProcessInstanceLockStrategy.java   |  15 ++
 .../lock/ProcessInstanceAtomicLockStrategy.java    |  94 ++++++++-
 .../impl/lock/ProcessInstanceLockStrategy.java     |  31 ++-
 .../kogito/app/jobs/api/JobSchedulerBuilder.java   |  10 +-
 .../kogito/app/jobs/impl/VertxJobScheduler.java    |  23 ++-
 .../integrations/ProcessInstanceJobExecutor.java   |   5 +-
 .../app/jobs/impl/VertxJobSchedulerTest.java       |  47 +++++
 .../quarkus/JDBCProcessInstancesFactory.java       |  31 +++
 .../org/kie/kogito/BranchExecutionCounter.java     |  49 +++++
 ...er.java => BranchExecutionCounterResource.java} |  28 ++-
 .../kogito/wih/BranchCounterWorkItemHandler.java   |  63 ++++++
 .../main/java/org/kie/kogito/wih/WIHRegister.java  |   1 +
 .../src/main/resources/asyncsplit.bpmn             | 223 +++++++++++++++++++++
 .../org/kie/kogito/it/OptimisticLockingTest.java   |  54 +++++
 .../pom.xml                                        |   8 +
 .../pom.xml                                        |  17 ++
 .../springboot/JDBCProcessInstancesFactory.java    |  22 ++
 .../org/kie/kogito/process/KogitoBeanProducer.java |   8 +-
 24 files changed, 793 insertions(+), 38 deletions(-)

diff --git 
a/kogito-addons/common/persistence/jdbc/src/main/java/org/kie/kogito/persistence/jdbc/JDBCProcessInstances.java
 
b/kogito-addons/common/persistence/jdbc/src/main/java/org/kie/kogito/persistence/jdbc/JDBCProcessInstances.java
index 414ac08c2a2..205a83e99fa 100644
--- 
a/kogito-addons/common/persistence/jdbc/src/main/java/org/kie/kogito/persistence/jdbc/JDBCProcessInstances.java
+++ 
b/kogito-addons/common/persistence/jdbc/src/main/java/org/kie/kogito/persistence/jdbc/JDBCProcessInstances.java
@@ -21,6 +21,7 @@ package org.kie.kogito.persistence.jdbc;
 import java.util.HashSet;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -49,6 +50,14 @@ public class JDBCProcessInstances<T extends Model> 
implements MutableProcessInst
     private final boolean lock;
     private final Repository repository;
 
+    /**
+     * Optional transaction registrar: when set, {@link #connectInstance} 
passes it to the
+     * process instance so write operations release the JVM lock only after 
the surrounding
+     * transaction commits. Null means "unlock immediately" — safe for 
non-transactional or
+     * test environments.
+     */
+    private Consumer<Runnable> transactionRegistrar;
+
     public JDBCProcessInstances(Process<?> process, DataSource dataSource, 
boolean lock) {
         this(process, dataSource, lock, null, null);
     }
@@ -65,6 +74,16 @@ public class JDBCProcessInstances<T extends Model> 
implements MutableProcessInst
         this.repository = new GenericRepository(dataSource, processes);
     }
 
+    /**
+     * Sets the transaction registrar to enable deferred lock release.
+     * Called by the framework-specific factory (Quarkus/Spring) when a 
transaction manager is
+     * available. The consumer receives an unlock {@link Runnable} and must 
arrange for it to
+     * run after the surrounding transaction commits (or immediately when no 
transaction is active).
+     */
+    public void setTransactionRegistrar(Consumer<Runnable> 
transactionRegistrar) {
+        this.transactionRegistrar = transactionRegistrar;
+    }
+
     @Override
     public boolean exists(String id) {
         return findById(id).isPresent();
@@ -96,7 +115,9 @@ public class JDBCProcessInstances<T extends Model> 
implements MutableProcessInst
         if (isActive(instance) || instance.status() == 
ProcessInstance.STATE_PENDING) {
             String[] eventTypes = getUniqueEvents(instance);
             if (lock) {
-                boolean isUpdated = repository.updateWithLock(process.id(), 
process.version(), UUID.fromString(id), 
marshaller.marshallProcessInstance(instance), instance.version(), eventTypes);
+                long versionBeforeWrite = instance.version();
+                boolean isUpdated = repository.updateWithLock(process.id(), 
process.version(), UUID.fromString(id), 
marshaller.marshallProcessInstance(instance), versionBeforeWrite, eventTypes);
+                LOGGER.trace("updateWithLock [{}] UPDATE WHERE version={} -> 
isUpdated={}", id, versionBeforeWrite, isUpdated);
                 if (!isUpdated) {
                     throw new ProcessInstanceOptimisticLockingException(id);
                 }
@@ -168,12 +189,16 @@ public class JDBCProcessInstances<T extends Model> 
implements MutableProcessInst
     }
 
     private void connectInstance(ProcessInstance<?> instance) {
-        ((AbstractProcessInstance<?>) instance).internalSetReloadSupplier(pi 
-> {
+        AbstractProcessInstance<?> api = (AbstractProcessInstance<?>) instance;
+        api.internalSetReloadSupplier(pi -> {
             Repository.Record r = repository.findByIdInternal(process.id(), 
process.version(), UUID.fromString(pi.id())).orElseThrow();
             pi.setVersion(r.version());
             marshaller.createdReloadFunction(r::payload).accept(pi);
             
pi.internalGetProcessInstance().setRootProcessId(r.rootProcessId());
             
pi.internalGetProcessInstance().setRootProcessVersion(r.rootProcessVersion());
         });
+        if (transactionRegistrar != null) {
+            api.internalSetTransactionRegistrar(transactionRegistrar);
+        }
     }
 }
diff --git 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
index 8013800618c..efec1b8b687 100644
--- 
a/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
+++ 
b/kogito-apps-quarkus/jobs-quarkus/kogito-addons-quarkus-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/quarkus/QuarkusJobsService.java
@@ -38,6 +38,7 @@ import org.kie.kogito.jobs.JobDescription;
 import org.kie.kogito.jobs.JobsService;
 
 import io.quarkus.runtime.Startup;
+import io.vertx.core.Vertx;
 
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PreDestroy;
@@ -98,6 +99,9 @@ public class QuarkusJobsService implements JobsService {
     @Inject
     TransactionRollbackMarker transactionRollbackMarker;
 
+    @Inject
+    Vertx vertx;
+
     @PostConstruct
     public void init() {
         this.jobScheduler = JobSchedulerBuilder.newJobSchedulerBuilder()
@@ -117,6 +121,7 @@ public class QuarkusJobsService implements JobsService {
                 .withTimeoutInterceptor(
                         new TransactionJobTimeoutInterceptor(),
                         new 
ErrorHandlingJobTimeoutInterceptor(exceptionHandlers.stream().toList()))
+                .withVertx(vertx)
                 .withExceptionDetailsExtractor(exceptionDetailsExtractor)
                 .withTransactionRollbackMarker(transactionRollbackMarker)
                 .withNumberOfWorkerThreads(numberOfWorkerThreads)
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
index b71251990fc..4d109d09176 100644
--- 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/SpringbootJobsService.java
@@ -46,6 +46,8 @@ import 
org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionSynchronization;
 import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 
+import io.vertx.core.Vertx;
+
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PreDestroy;
 
@@ -97,6 +99,9 @@ public class SpringbootJobsService implements JobsService {
     @Autowired
     protected TransactionRollbackMarker transactionRollbackMarker;
 
+    @Autowired(required = false)
+    protected Vertx vertx;
+
     @PostConstruct
     public void init() {
         this.jobScheduler = JobSchedulerBuilder.newJobSchedulerBuilder()
@@ -116,6 +121,7 @@ public class SpringbootJobsService implements JobsService {
                 .withTimeoutInterceptor(
                         new 
ErrorHandlingJobTimeoutInterceptor(ofNullable(exceptionHandlers).stream().toList()),
                         new 
TransactionJobTimeoutInterceptor(transactionManager))
+                .withVertx(vertx)
                 .withExceptionDetailsExtractor(exceptionDetailsExtractor)
                 .withTransactionRollbackMarker(transactionRollbackMarker)
                 .withNumberOfWorkerThreads(numberOfWorkerThreads)
diff --git 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
index f2ef72f0839..70d708d9a83 100644
--- 
a/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
+++ 
b/kogito-apps-springboot/jobs-springboot/kogito-addons-springboot-embedded-jobs/src/main/java/org/kie/kogito/app/jobs/springboot/TransactionJobTimeoutInterceptor.java
@@ -23,15 +23,16 @@ import java.util.concurrent.Callable;
 import org.kie.kogito.app.jobs.api.JobTimeoutExecution;
 import org.kie.kogito.app.jobs.api.JobTimeoutInterceptor;
 import org.springframework.transaction.PlatformTransactionManager;
-import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.support.TransactionTemplate;
 
 public class TransactionJobTimeoutInterceptor implements JobTimeoutInterceptor 
{
 
-    private TransactionTemplate transactionTemplate;
+    private final TransactionTemplate transactionTemplate;
 
     public TransactionJobTimeoutInterceptor(PlatformTransactionManager 
transactionManager) {
         this.transactionTemplate = new TransactionTemplate(transactionManager);
+        
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
     }
 
     @Override
@@ -41,25 +42,14 @@ public class TransactionJobTimeoutInterceptor implements 
JobTimeoutInterceptor {
             @Override
             public JobTimeoutExecution call() throws Exception {
                 return transactionTemplate.execute(status -> {
+                    
SpringBootTransactionRollbackMarker.setCurrentTransactionStatus(status);
                     try {
-                        // Initialize transaction synchronization if not 
already active
-                        // This is needed for 
TransactionSynchronizationManager callbacks to work
-                        if 
(!TransactionSynchronizationManager.isSynchronizationActive()) {
-                            
TransactionSynchronizationManager.initSynchronization();
-                        }
-
-                        // Store the transaction status in thread-local for 
SpringBootTransactionRollbackMarker
-                        
SpringBootTransactionRollbackMarker.setCurrentTransactionStatus(status);
-
                         return callable.call();
                     } catch (RuntimeException e) {
-                        // Re-throw RuntimeException to trigger rollback
                         throw e;
                     } catch (Exception e) {
-                        // Wrap checked exceptions in RuntimeException to 
trigger rollback
                         throw new RuntimeException(e);
                     } finally {
-                        // Clear the transaction status from thread-local
                         
SpringBootTransactionRollbackMarker.clearCurrentTransactionStatus();
                     }
                 });
diff --git 
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/AsyncEventNodeInstance.java
 
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/AsyncEventNodeInstance.java
index bd82b536af5..a4d7d624e34 100644
--- 
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/AsyncEventNodeInstance.java
+++ 
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/node/AsyncEventNodeInstance.java
@@ -22,7 +22,6 @@ import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.Collection;
 import java.util.Objects;
-import java.util.Optional;
 
 import org.jbpm.process.instance.InternalProcessRuntime;
 import org.jbpm.workflow.instance.NodeInstanceContainer;
@@ -112,7 +111,7 @@ public class AsyncEventNodeInstance extends 
EventNodeInstance {
                                     
.processVersion(instance.getProcessInstance().getProcessVersion())
                                     
.rootProcessId(instance.getProcessInstance().getRootProcessId())
                                     
.rootProcessVersion(instance.getProcessInstance().getRootProcessVersion())
-                                    
.nodeInstanceId(Optional.ofNullable(from).map(KogitoNodeInstance::getStringId).orElse(null))
+                                    .nodeInstanceId(this.getStringId())
                                     .build();
                     JobsService jobService = processRuntime.getJobsService();
                     String jobId = jobService.scheduleJob(jobDescription);
diff --git 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
index 842fdb08ffc..e7925c88cab 100644
--- 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
+++ 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
@@ -111,6 +111,23 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
 
     private ProcessInstanceLockStrategy processInstanceLockStrategy;
 
+    /**
+     * Transaction registrar for deferred lock release. When non-null (set by
+     * {@link JDBCProcessInstances#connectInstance} via {@link 
#internalSetTransactionRegistrar}),
+     * write operations defer the JVM-level lock release until after the 
surrounding transaction
+     * commits, preventing other threads from reading a stale optimistic-lock 
version.
+     */
+    private Consumer<Runnable> transactionRegistrar;
+
+    /**
+     * Sets the transaction registrar so that write operations release the JVM 
lock only after
+     * the surrounding transaction commits. Called by the persistence layer 
after the instance
+     * is unmarshalled from the DB. A {@code null} value (the default) means 
unlock immediately.
+     */
+    public void internalSetTransactionRegistrar(Consumer<Runnable> 
transactionRegistrar) {
+        this.transactionRegistrar = transactionRegistrar;
+    }
+
     public AbstractProcessInstance(AbstractProcess<T> process, T variables, 
ProcessRuntime rt) {
         this(process, variables, null, rt);
     }
@@ -612,11 +629,26 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
     }
 
     private <R> R 
executeInWorkflowProcessInstance(Function<WorkflowProcessInstanceImpl, R> 
execution) {
-        // Check if this is a reentrant call before entering the lock
+        // Check reentrance before acquiring the lock: isLockedByCurrentThread 
returns true only
+        // when the current thread *already* holds the lock, so it must be 
read before
+        // executeWriteOperation acquires it — otherwise every outermost call 
looks reentrant.
         boolean isReentrant = 
processInstanceLockStrategy.isLockedByCurrentThread(id);
-
-        return processInstanceLockStrategy.executeOperation(id, () -> {
+        return processInstanceLockStrategy.executeWriteOperation(id, () -> {
+            // The process instance (and its optimistic-lock version) may have 
been loaded from
+            // the DB *before* this lock was acquired (e.g. by 
JDBCProcessInstances.findById in
+            // ProcessInstanceJobExecutor). Discard that stale snapshot so 
that the SELECT issued
+            // by reloadSupplier happens *inside* the lock, guaranteeing the 
version we write back
+            // was read under mutual exclusion with every other writer.
+            // Guards:
+            //   !isReentrant — reentrant callers must not null out the 
parent's live state.
+            //   reloadSupplier != null — brand-new instances (start path) 
have no supplier yet;
+            //                           their processInstance was set in the 
constructor.
+            if (!isReentrant && reloadSupplier != null) {
+                LOG.trace("executeInWorkflowProcessInstance [{}] discarding 
stale snapshot (version={}) before lock-guarded reload", id, this.version);
+                this.processInstance = null;
+            }
             WorkflowProcessInstanceImpl workflowProcessInstance = 
internalLoadProcessInstanceState();
+            LOG.trace("executeInWorkflowProcessInstance [{}] state loaded, 
version={}, reentrant={}", id, this.version, isReentrant);
             if (isProcessInstanceConnected()) {
                 
getProcessRuntime().getProcessInstanceManager().addProcessInstance(workflowProcessInstance);
             }
@@ -636,6 +668,7 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
             }
 
             if (isProcessInstanceConnected()) {
+                LOG.trace("executeInWorkflowProcessInstance [{}] about to 
syncPersistence with version={}", id, this.version);
                 syncPersistence(workflowProcessInstance);
                 
getProcessRuntime().getProcessInstanceManager().removeProcessInstance(workflowProcessInstance);
             }
@@ -646,7 +679,7 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
             }
 
             return outcome;
-        });
+        }, transactionRegistrar);
     }
 
     @Override
diff --git 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
index eabf6a15e13..92bd63a7782 100644
--- 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
+++ 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
@@ -18,6 +18,8 @@
  */
 package org.kie.kogito.process.impl.lock;
 
+import java.util.function.Consumer;
+
 import org.kie.kogito.services.context.ProcessInstanceContext;
 
 /**
@@ -67,6 +69,19 @@ public class ContextAwareProcessInstanceLockStrategy 
implements ProcessInstanceL
         });
     }
 
+    @Override
+    public <T> T executeWriteOperation(String processInstanceId, 
WorkflowAtomicExecutor<T> operation,
+            Consumer<Runnable> transactionRegistrar) {
+        return delegate.executeWriteOperation(processInstanceId, () -> {
+            ProcessInstanceContext.setProcessInstanceId(processInstanceId);
+            try {
+                return operation.execute();
+            } finally {
+                ProcessInstanceContext.clear();
+            }
+        }, transactionRegistrar);
+    }
+
     @Override
     public boolean isLockedByCurrentThread(String processInstanceId) {
         return delegate.isLockedByCurrentThread(processInstanceId);
diff --git 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
index 0cd3a275317..4cf9b1847fc 100644
--- 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
+++ 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
@@ -21,6 +21,7 @@ package org.kie.kogito.process.impl.lock;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -101,7 +102,7 @@ public class ProcessInstanceAtomicLockStrategy implements 
ProcessInstanceLockStr
                 LOG.trace("Lock released for {}", processInstanceId);
             }
 
-            // evaluate atomically if the lock is still in used before 
removing it.
+            // evaluate atomically if the lock is still in use before removing 
it.
             locks.computeIfPresent(processInstanceId, (pid, holder) -> {
                 holder.removeReference();
                 if (holder.isReferenced()) {
@@ -112,7 +113,96 @@ public class ProcessInstanceAtomicLockStrategy implements 
ProcessInstanceLockStr
                 }
             });
         }
+    }
+
+    /**
+     * Write path with optional deferred unlock.
+     *
+     * <p>
+     * When {@code transactionRegistrar} is non-null and the current thread 
does not already
+     * hold the lock (non-reentrant call), the lock release is deferred to 
after the surrounding
+     * transaction commits. The registrar receives an unlock {@link Runnable} 
and must arrange
+     * for it to run in an {@code afterCompletion}/{@code afterCommit} 
callback; when no active
+     * transaction is present the registrar must run the action immediately.
+     *
+     * <p>
+     * Reentrant calls (the lock is already held by the current thread) always 
release
+     * immediately in the {@code finally} block, regardless of {@code 
transactionRegistrar},
+     * because the outermost non-reentrant frame is responsible for the 
deferred release.
+     *
+     * <p>
+     * On the exception path the lock is always released immediately so that 
other threads
+     * are not blocked behind a failed operation.
+     */
+    @Override
+    public <T> T executeWriteOperation(String processInstanceId, 
WorkflowAtomicExecutor<T> executor,
+            Consumer<Runnable> transactionRegistrar) {
+        if (transactionRegistrar == null) {
+            return executeOperation(processInstanceId, executor);
+        }
+
+        boolean alreadyHeld = isLockedByCurrentThread(processInstanceId);
+
+        ProcessInstanceLockHolder holder = locks.compute(processInstanceId, 
(pid, h) -> {
+            ProcessInstanceLockHolder newHolder = h == null ? new 
ProcessInstanceLockHolder() : h;
+            newHolder.addReference();
+            LOG.trace("Creating lock {} from list as none is waiting for it by 
{}", newHolder.lock, pid);
+            return newHolder;
+        });
 
+        boolean alreadyAcquired = holder.isHeldByCurrentThread();
+        boolean deferred = false;
+        try {
+            if (!alreadyAcquired) {
+                LOG.trace("About to acquire lock for {}", processInstanceId);
+            }
+            holder.lock();
+            if (!alreadyAcquired) {
+                LOG.trace("Lock acquired for {}", processInstanceId);
+            }
+
+            T result = executor.execute();
+
+            if (!alreadyHeld) {
+                // Defer lock release to after the surrounding transaction 
commits.
+                // The registrar captures the holder reference so the 
post-commit callback
+                // can unlock it even after this stack frame has returned.
+                final ProcessInstanceLockHolder deferredHolder = holder;
+                LOG.trace("Deferring lock release to post-commit for {}", 
processInstanceId);
+                transactionRegistrar.accept(() -> {
+                    deferredHolder.unlock();
+                    LOG.trace("Lock released (post-commit) for {}", 
processInstanceId);
+                    locks.computeIfPresent(processInstanceId, (pid, h) -> {
+                        h.removeReference();
+                        if (h.isReferenced()) {
+                            return h;
+                        } else {
+                            LOG.trace("Removing lock {} from list as none is 
waiting for it by {}", h.lock, pid);
+                            return null;
+                        }
+                    });
+                });
+                deferred = true;
+            }
+            return result;
+        } finally {
+            if (!deferred) {
+                // Reentrant call, or exception path: release immediately.
+                holder.unlock();
+                if (!alreadyAcquired) {
+                    LOG.trace("Lock released for {}", processInstanceId);
+                }
+                locks.computeIfPresent(processInstanceId, (pid, h) -> {
+                    h.removeReference();
+                    if (h.isReferenced()) {
+                        return h;
+                    } else {
+                        LOG.trace("Removing lock {} from list as none is 
waiting for it by {}", h.lock, pid);
+                        return null;
+                    }
+                });
+            }
+        }
     }
 
     @Override
@@ -121,7 +211,7 @@ public class ProcessInstanceAtomicLockStrategy implements 
ProcessInstanceLockStr
         return holder != null && holder.isHeldByCurrentThread();
     }
 
-    public static synchronized ProcessInstanceLockStrategy instance() {
+    public static synchronized ProcessInstanceAtomicLockStrategy instance() {
         if (INSTANCE == null) {
             INSTANCE = new ProcessInstanceAtomicLockStrategy();
         }
diff --git 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
index 5d0a27d5aaa..a9e3605f307 100644
--- 
a/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
+++ 
b/kogito-jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
@@ -18,9 +18,38 @@
  */
 package org.kie.kogito.process.impl.lock;
 
+import java.util.function.Consumer;
+
 public interface ProcessInstanceLockStrategy {
 
-    <T> T executeOperation(String processInstanceId, WorkflowAtomicExecutor<T> 
empty);
+    <T> T executeOperation(String processInstanceId, WorkflowAtomicExecutor<T> 
executor);
+
+    /**
+     * Executes a write operation under the lock, optionally deferring the 
lock release to after
+     * the surrounding transaction commits.
+     *
+     * <p>
+     * When {@code transactionRegistrar} is non-null, implementations should 
arrange for the
+     * lock to be released only after the surrounding transaction commits, by 
passing an unlock
+     * {@link Runnable} to the registrar. The registrar must run the action 
immediately when no
+     * active transaction is present, so this is safe in non-transactional 
environments too.
+     *
+     * <p>
+     * The default implementation ignores {@code transactionRegistrar} and 
delegates to
+     * {@link #executeOperation}, making it safe for non-transactional or test 
environments.
+     *
+     * @param processInstanceId the process instance id
+     * @param executor the operation to execute
+     * @param transactionRegistrar accepts an unlock {@link Runnable} and 
arranges for it to run
+     *        after the surrounding transaction commits; {@code null} means
+     *        unlock immediately in the {@code finally} block
+     * @param <T> the return type
+     * @return the result of the operation
+     */
+    default <T> T executeWriteOperation(String processInstanceId, 
WorkflowAtomicExecutor<T> executor,
+            Consumer<Runnable> transactionRegistrar) {
+        return executeOperation(processInstanceId, executor);
+    }
 
     /**
      * Checks if the current thread already holds the lock for the given 
process instance.
diff --git 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
index 8376af6256e..5432cc09357 100644
--- 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
+++ 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/api/JobSchedulerBuilder.java
@@ -25,10 +25,12 @@ import org.kie.kogito.app.jobs.spi.JobStore;
 import org.kie.kogito.app.jobs.spi.TransactionRollbackMarker;
 import org.kie.kogito.event.EventPublisher;
 
+import io.vertx.core.Vertx;
+
 public interface JobSchedulerBuilder {
 
     static JobSchedulerBuilder newJobSchedulerBuilder() {
-        return new VertxJobScheduler().new VertxJobSchedulerBuilder();
+        return VertxJobScheduler.builder();
     }
 
     JobScheduler build();
@@ -57,6 +59,12 @@ public interface JobSchedulerBuilder {
 
     JobSchedulerBuilder withNumberOfWorkerThreads(Integer 
numberOfWorkerThreads);
 
+    /**
+     * Supplies the platform-managed Vert.x instance to use as the execution 
runtime.
+     * If not called, the scheduler creates and owns a standalone instance.
+     */
+    JobSchedulerBuilder withVertx(Vertx vertx);
+
     JobSchedulerBuilder withJobSynchronization(JobSynchronization 
jobSynchronization);
 
     JobSchedulerBuilder withJobDescriptorMergers(JobDescriptionMerger... 
jobDescriptionMergers);
diff --git 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
index 8a5f284acce..0b3bcb9bfcc 100644
--- 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
+++ 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/impl/VertxJobScheduler.java
@@ -91,6 +91,8 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     private Vertx vertx;
 
+    private boolean ownsVertx;
+
     private WorkerExecutor workerExecutor;
 
     private JobContextFactory jobContextFactory;
@@ -205,6 +207,12 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
             return this;
         }
 
+        @Override
+        public JobSchedulerBuilder withVertx(Vertx vertx) {
+            VertxJobScheduler.this.vertx = vertx;
+            return this;
+        }
+
         @Override
         public JobSchedulerBuilder 
withJobDescriptorMergers(JobDescriptionMerger... jobDescriptionMergers) {
             
VertxJobScheduler.this.jobDescriptionMergers.addAll(List.of(jobDescriptionMergers));
@@ -225,6 +233,10 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     }
 
+    public static VertxJobSchedulerBuilder builder() {
+        return new VertxJobScheduler().new VertxJobSchedulerBuilder();
+    }
+
     public VertxJobScheduler() {
         this.jobExecutors = new ArrayList<>();
         this.jobStore = new MemoryJobStore();
@@ -326,7 +338,11 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
     @Override
     public void init() {
-        this.vertx = Vertx.builder().build();
+        if (this.vertx == null) {
+            LOG.info("No Vert.x instance provided — creating a standalone one 
with default settings.");
+            this.vertx = Vertx.builder().build();
+            this.ownsVertx = true;
+        }
         this.workerExecutor = this.vertx.createSharedWorkerExecutor("Jobs", 
numberOfWorkerThreads);
         this.maxRefreshJobsIntervalWindow = 
Math.max(maxRefreshJobsIntervalWindow, refreshJobsInterval);
         this.refreshJobsIntervalTimerId = this.vertx.setPeriodic(0L, 
refreshJobsInterval, this);
@@ -352,12 +368,15 @@ public class VertxJobScheduler implements JobScheduler, 
Handler<Long> {
 
         // clean up
         this.workerExecutor.close();
-        this.vertx.close();
+        if (this.ownsVertx) {
+            this.vertx.close();
+        }
         this.jobsScheduled.clear();
 
         this.refreshJobsIntervalTimerId = null;
         this.workerExecutor = null;
         this.vertx = null;
+        this.ownsVertx = false;
     }
 
     @Override
diff --git 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ProcessInstanceJobExecutor.java
 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ProcessInstanceJobExecutor.java
index c2367b285cc..21ed9fc6141 100644
--- 
a/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ProcessInstanceJobExecutor.java
+++ 
b/kogito-jobs/jobs-common-embedded/src/main/java/org/kie/kogito/app/jobs/integrations/ProcessInstanceJobExecutor.java
@@ -32,9 +32,8 @@ import org.kie.kogito.uow.UnitOfWorkManager;
 public class ProcessInstanceJobExecutor implements JobExecutor {
     public static final String SIGNAL = "timerTriggered";
 
-    private Processes processes;
-
-    private UnitOfWorkManager uom;
+    private final Processes processes;
+    private final UnitOfWorkManager uom;
 
     public ProcessInstanceJobExecutor(Processes processes, UnitOfWorkManager 
unitOfWorkManager) {
         this.processes = processes;
diff --git 
a/kogito-jobs/jobs-common-embedded/src/test/java/org/kie/kogito/app/jobs/impl/VertxJobSchedulerTest.java
 
b/kogito-jobs/jobs-common-embedded/src/test/java/org/kie/kogito/app/jobs/impl/VertxJobSchedulerTest.java
index 17afb1e2892..d8ccb384b5b 100644
--- 
a/kogito-jobs/jobs-common-embedded/src/test/java/org/kie/kogito/app/jobs/impl/VertxJobSchedulerTest.java
+++ 
b/kogito-jobs/jobs-common-embedded/src/test/java/org/kie/kogito/app/jobs/impl/VertxJobSchedulerTest.java
@@ -40,10 +40,57 @@ import org.kie.kogito.jobs.service.model.JobStatus;
 import org.kie.kogito.jobs.service.model.ScheduledJob;
 import org.kie.kogito.timer.impl.SimpleTimerTrigger;
 
+import io.vertx.core.Vertx;
+
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class VertxJobSchedulerTest {
 
+    /**
+     * Verifies that when a platform-managed Vert.x instance is supplied via
+     * {@link JobSchedulerBuilder#withVertx(Vertx)}, the scheduler uses it 
instead of
+     * creating a standalone instance. The supplied Vert.x is closed by the 
caller, not
+     * by the scheduler — verified by asserting the scheduler completes 
normally and the
+     * external instance is still running after {@code jobScheduler.close()}.
+     */
+    @Test
+    public void testWithExternalVertxUsesSuppliedInstance() throws Exception {
+        final String jobId = "external-vertx-test";
+        Vertx externalVertx = Vertx.vertx();
+        try {
+            LatchExecutionJobSchedulerListener listener = new 
LatchExecutionJobSchedulerListener();
+            TestJobExecutor executor = new TestJobExecutor();
+            JobStore jobStore = new MemoryJobStore();
+            JobContextFactory jobContextFactory = new 
MemoryJobContextFactory();
+
+            JobScheduler jobScheduler = 
JobSchedulerBuilder.newJobSchedulerBuilder()
+                    .withJobExecutors(executor)
+                    .withJobEventAdapters(new TestJobDetailsEventAdapter())
+                    .withEventPublishers(new TestEventPublisher())
+                    .withJobContextFactory(jobContextFactory)
+                    .withJobStore(jobStore)
+                    .withJobSchedulerListeners(listener)
+                    .withJobDescriptorMergers(new TestJobDescriptionMerger())
+                    .withVertx(externalVertx)
+                    .build();
+
+            jobScheduler.init();
+            jobScheduler.schedule(new TestJobDescription(jobId, 
ZonedDateTime.now().plus(Duration.ofSeconds(1))));
+            listener.waitForExecution();
+
+            assertThat(executor.getJobsExecuted()).hasSize(1);
+            assertThat(listener.isExecuted()).isTrue();
+
+            // close the scheduler — must NOT close the externally supplied 
Vert.x
+            jobScheduler.close();
+
+            // external Vert.x is still usable (would throw/fail if already 
closed)
+            assertThat(externalVertx.deploymentIDs()).isNotNull();
+        } finally {
+            externalVertx.close();
+        }
+    }
+
     @Test
     public void testBasicExactTimeFlow() throws Exception {
         final String jobId = "1";
diff --git 
a/kogito-quarkus/addons/persistence/jdbc/runtime/src/main/java/org/kie/kogito/persistence/quarkus/JDBCProcessInstancesFactory.java
 
b/kogito-quarkus/addons/persistence/jdbc/runtime/src/main/java/org/kie/kogito/persistence/quarkus/JDBCProcessInstancesFactory.java
index 34a485f15dc..1aed7784939 100644
--- 
a/kogito-quarkus/addons/persistence/jdbc/runtime/src/main/java/org/kie/kogito/persistence/quarkus/JDBCProcessInstancesFactory.java
+++ 
b/kogito-quarkus/addons/persistence/jdbc/runtime/src/main/java/org/kie/kogito/persistence/quarkus/JDBCProcessInstancesFactory.java
@@ -26,15 +26,21 @@ import javax.sql.DataSource;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.kie.kogito.internal.process.runtime.HeadersPersistentConfig;
 import org.kie.kogito.persistence.jdbc.AbstractProcessInstancesFactory;
+import org.kie.kogito.persistence.jdbc.JDBCProcessInstances;
+import org.kie.kogito.process.Process;
 import org.kie.kogito.process.Processes;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
+import jakarta.transaction.TransactionSynchronizationRegistry;
 
 @ApplicationScoped
 public class JDBCProcessInstancesFactory extends 
AbstractProcessInstancesFactory {
 
+    @Inject
+    Instance<TransactionSynchronizationRegistry> txSyncRegistry;
+
     @Inject
     public JDBCProcessInstancesFactory(DataSource dataSource,
             @ConfigProperty(name = "kogito.persistence.optimistic.lock", 
defaultValue = "false") Boolean lock,
@@ -48,4 +54,29 @@ public class JDBCProcessInstancesFactory extends 
AbstractProcessInstancesFactory
     public JDBCProcessInstancesFactory() {
     }
 
+    @Override
+    public JDBCProcessInstances<?> createProcessInstances(Process<?> process) {
+        JDBCProcessInstances<?> instances = 
super.createProcessInstances(process);
+        if (txSyncRegistry != null && txSyncRegistry.isResolvable()) {
+            TransactionSynchronizationRegistry registry = txSyncRegistry.get();
+            instances.setTransactionRegistrar(unlockAction -> {
+                if (registry.getTransactionStatus() != 
jakarta.transaction.Status.STATUS_NO_TRANSACTION) {
+                    registry.registerInterposedSynchronization(new 
jakarta.transaction.Synchronization() {
+                        @Override
+                        public void beforeCompletion() {
+                        }
+
+                        @Override
+                        public void afterCompletion(int status) {
+                            unlockAction.run();
+                        }
+                    });
+                } else {
+                    unlockAction.run();
+                }
+            });
+        }
+        return instances;
+    }
+
 }
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounter.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounter.java
new file mode 100644
index 00000000000..a2a07d295c6
--- /dev/null
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounter.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.inject.Singleton;
+
+/**
+ * Non-transactional attempt counter for the asyncsplit integration test.
+ * Incremented by BranchCounterWorkItemHandler on every execution attempt,
+ * including those whose surrounding JTA transaction is later rolled back.
+ * Because this state lives in a @Singleton CDI bean (not a DB row), it is
+ * never rolled back and therefore reliably reflects the total number of
+ * branch executions, not just committed ones.
+ */
+@Singleton
+public class BranchExecutionCounter {
+
+    private final AtomicInteger count = new AtomicInteger(0);
+
+    public int increment() {
+        return count.incrementAndGet();
+    }
+
+    public int get() {
+        return count.get();
+    }
+
+    public void reset() {
+        count.set(0);
+    }
+}
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounterResource.java
similarity index 64%
copy from 
kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
copy to 
kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounterResource.java
index d0162926801..eb775c031a4 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/BranchExecutionCounterResource.java
@@ -16,15 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.kie.kogito.wih;
-
-import org.kie.kogito.process.impl.DefaultWorkItemHandlerConfig;
+package org.kie.kogito;
 
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
 
 @ApplicationScoped
-public class WIHRegister extends DefaultWorkItemHandlerConfig {
-    {
-        register("CustomTask", new CustomWorkItemHandler());
+@Path("/branch-counter")
+public class BranchExecutionCounterResource {
+
+    @Inject
+    BranchExecutionCounter counter;
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public int get() {
+        return counter.get();
+    }
+
+    @DELETE
+    public void reset() {
+        counter.reset();
     }
 }
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/BranchCounterWorkItemHandler.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/BranchCounterWorkItemHandler.java
new file mode 100644
index 00000000000..8689919fd62
--- /dev/null
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/BranchCounterWorkItemHandler.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.wih;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.kie.kogito.BranchExecutionCounter;
+import org.kie.kogito.internal.process.workitem.KogitoWorkItem;
+import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler;
+import org.kie.kogito.internal.process.workitem.KogitoWorkItemManager;
+import org.kie.kogito.internal.process.workitem.WorkItemTransition;
+import org.kie.kogito.process.workitems.impl.DefaultKogitoWorkItemHandler;
+
+import io.quarkus.arc.Arc;
+
+/**
+ * Work item handler for the asyncsplit race-condition reproducer.
+ * Sleeps for {@code waitTime} ms (to widen the race window) then increments
+ * a non-transactional {@link BranchExecutionCounter} singleton.
+ * Because the counter is outside the JTA transaction, its value reflects
+ * total execution attempts — not just committed ones — making it the correct
+ * observable for detecting optimistic-locking retries.
+ */
+public class BranchCounterWorkItemHandler extends DefaultKogitoWorkItemHandler 
{
+
+    @Override
+    public Optional<WorkItemTransition> 
activateWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler 
handler,
+            KogitoWorkItem workItem, WorkItemTransition transition) {
+        Long waitTime = (Long) workItem.getParameter("waitTime");
+        if (waitTime != null && waitTime > 0) {
+            try {
+                Thread.sleep(waitTime);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        Arc.container().select(BranchExecutionCounter.class).get().increment();
+        return 
Optional.of(handler.completeTransition(workItem.getPhaseStatus(), 
Collections.emptyMap()));
+    }
+
+    @Override
+    public Optional<WorkItemTransition> 
abortWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler 
handler,
+            KogitoWorkItem workItem, WorkItemTransition transition) {
+        return Optional.of(handler.abortTransition(workItem.getPhaseStatus()));
+    }
+}
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
index d0162926801..3e21a9902d9 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/java/org/kie/kogito/wih/WIHRegister.java
@@ -26,5 +26,6 @@ import jakarta.enterprise.context.ApplicationScoped;
 public class WIHRegister extends DefaultWorkItemHandlerConfig {
     {
         register("CustomTask", new CustomWorkItemHandler());
+        register("BranchCounter", new BranchCounterWorkItemHandler());
     }
 }
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/resources/asyncsplit.bpmn
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/resources/asyncsplit.bpmn
new file mode 100644
index 00000000000..281ed5e5351
--- /dev/null
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/main/resources/asyncsplit.bpmn
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<!--
+  Reproducer for the parallel-async-split stale-lock bug:
+
+  Start → async TimerReplacement → parallel split → async Branch1 + async 
Branch2
+  (both sleep waitTime ms and increment a non-transactional counter) → 
parallel join → End
+
+  With waitTime=100 both branches finish nearly simultaneously and race to 
signal the joining
+  gateway. The non-transactional BranchExecutionCounter reflects every 
execution attempt,
+  including those whose JTA transaction was later rolled back. The test 
asserts counter == 2,
+  meaning each branch ran exactly once with no retries.
+-->
+<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns="http://www.omg.org/bpmn20";
+    xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL";
+    xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI";
+    xmlns:bpsim="http://www.bpsim.org/schemas/1.0";
+    xmlns:dc="http://www.omg.org/spec/DD/20100524/DC";
+    xmlns:di="http://www.omg.org/spec/DD/20100524/DI";
+    xmlns:drools="http://www.jboss.org/drools";
+    id="_asyncsplit"
+    xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd 
http://www.jboss.org/drools drools.xsd http://www.bpsim.org/schemas/1.0 
bpsim.xsd http://www.omg.org/spec/DD/20100524/DC DC.xsd 
http://www.omg.org/spec/DD/20100524/DI DI.xsd"
+    exporter="jBPM Process Modeler" exporterVersion="2.0" 
targetNamespace="http://www.omg.org/bpmn20";>
+
+  <bpmn2:itemDefinition id="_waitTimeItem" structureRef="Long" />
+
+  <bpmn2:collaboration id="_collaboration" name="Default Collaboration">
+    <bpmn2:participant id="_participant" name="Pool Participant" 
processRef="asyncsplit"/>
+  </bpmn2:collaboration>
+
+  <bpmn2:process id="asyncsplit" drools:packageName="org.kie.kogito" 
drools:version="1.0" drools:adHoc="false"
+      name="asyncsplit" isExecutable="true" processType="Public">
+
+    <bpmn2:property id="waitTime" itemSubjectRef="_waitTimeItem" 
name="waitTime" />
+
+    <bpmn2:startEvent id="_Start">
+      <bpmn2:outgoing>_f1</bpmn2:outgoing>
+    </bpmn2:startEvent>
+
+    <!-- Async script that initialises nothing — kept async to reproduce the 
job-scheduling pattern -->
+    <bpmn2:scriptTask id="_Timer" name="TimerReplacement" 
scriptFormat="http://www.java.com/java";>
+      <bpmn2:extensionElements>
+        <drools:metaData 
name="elementname"><drools:metaValue><![CDATA[TimerReplacement]]></drools:metaValue></drools:metaData>
+        <drools:metaData 
name="customAsync"><drools:metaValue><![CDATA[true]]></drools:metaValue></drools:metaData>
+      </bpmn2:extensionElements>
+      <bpmn2:incoming>_f1</bpmn2:incoming>
+      <bpmn2:outgoing>_f2</bpmn2:outgoing>
+      <bpmn2:script><![CDATA[// intentionally empty]]></bpmn2:script>
+    </bpmn2:scriptTask>
+
+    <bpmn2:parallelGateway id="_Split" gatewayDirection="Diverging">
+      <bpmn2:incoming>_f2</bpmn2:incoming>
+      <bpmn2:outgoing>_f3</bpmn2:outgoing>
+      <bpmn2:outgoing>_f4</bpmn2:outgoing>
+    </bpmn2:parallelGateway>
+
+    <!--
+      Branch1: async work item that sleeps waitTime ms then increments the 
non-transactional counter.
+      customAsync=true causes this to be scheduled as a separate job, creating 
the race condition.
+    -->
+    <bpmn2:task id="_Branch1" drools:taskName="BranchCounter" name="Branch1">
+      <bpmn2:extensionElements>
+        <drools:metaData 
name="elementname"><drools:metaValue><![CDATA[Branch1]]></drools:metaValue></drools:metaData>
+        <drools:metaData 
name="customAsync"><drools:metaValue><![CDATA[true]]></drools:metaValue></drools:metaData>
+      </bpmn2:extensionElements>
+      <bpmn2:incoming>_f3</bpmn2:incoming>
+      <bpmn2:outgoing>_f5a</bpmn2:outgoing>
+      <bpmn2:ioSpecification>
+        <bpmn2:dataInput id="_Branch1_waitTimeInput" drools:dtype="Long" 
name="waitTime"/>
+        <bpmn2:dataInput id="_Branch1_TaskNameInput" drools:dtype="Object" 
name="TaskName"/>
+        <bpmn2:inputSet>
+          <bpmn2:dataInputRefs>_Branch1_waitTimeInput</bpmn2:dataInputRefs>
+          <bpmn2:dataInputRefs>_Branch1_TaskNameInput</bpmn2:dataInputRefs>
+        </bpmn2:inputSet>
+        <bpmn2:outputSet/>
+      </bpmn2:ioSpecification>
+      <bpmn2:dataInputAssociation>
+        <bpmn2:sourceRef>waitTime</bpmn2:sourceRef>
+        <bpmn2:targetRef>_Branch1_waitTimeInput</bpmn2:targetRef>
+      </bpmn2:dataInputAssociation>
+      <bpmn2:dataInputAssociation>
+        <bpmn2:targetRef>_Branch1_TaskNameInput</bpmn2:targetRef>
+        <bpmn2:assignment>
+          <bpmn2:from 
xsi:type="bpmn2:tFormalExpression"><![CDATA[BranchCounter]]></bpmn2:from>
+          <bpmn2:to 
xsi:type="bpmn2:tFormalExpression"><![CDATA[_Branch1_TaskNameInput]]></bpmn2:to>
+        </bpmn2:assignment>
+      </bpmn2:dataInputAssociation>
+    </bpmn2:task>
+
+    <!-- Branch2: identical to Branch1, races it to the parallel join -->
+    <bpmn2:task id="_Branch2" drools:taskName="BranchCounter" name="Branch2">
+      <bpmn2:extensionElements>
+        <drools:metaData 
name="elementname"><drools:metaValue><![CDATA[Branch2]]></drools:metaValue></drools:metaData>
+        <drools:metaData 
name="customAsync"><drools:metaValue><![CDATA[true]]></drools:metaValue></drools:metaData>
+      </bpmn2:extensionElements>
+      <bpmn2:incoming>_f4</bpmn2:incoming>
+      <bpmn2:outgoing>_f5b</bpmn2:outgoing>
+      <bpmn2:ioSpecification>
+        <bpmn2:dataInput id="_Branch2_waitTimeInput" drools:dtype="Long" 
name="waitTime"/>
+        <bpmn2:dataInput id="_Branch2_TaskNameInput" drools:dtype="Object" 
name="TaskName"/>
+        <bpmn2:inputSet>
+          <bpmn2:dataInputRefs>_Branch2_waitTimeInput</bpmn2:dataInputRefs>
+          <bpmn2:dataInputRefs>_Branch2_TaskNameInput</bpmn2:dataInputRefs>
+        </bpmn2:inputSet>
+        <bpmn2:outputSet/>
+      </bpmn2:ioSpecification>
+      <bpmn2:dataInputAssociation>
+        <bpmn2:sourceRef>waitTime</bpmn2:sourceRef>
+        <bpmn2:targetRef>_Branch2_waitTimeInput</bpmn2:targetRef>
+      </bpmn2:dataInputAssociation>
+      <bpmn2:dataInputAssociation>
+        <bpmn2:targetRef>_Branch2_TaskNameInput</bpmn2:targetRef>
+        <bpmn2:assignment>
+          <bpmn2:from 
xsi:type="bpmn2:tFormalExpression"><![CDATA[BranchCounter]]></bpmn2:from>
+          <bpmn2:to 
xsi:type="bpmn2:tFormalExpression"><![CDATA[_Branch2_TaskNameInput]]></bpmn2:to>
+        </bpmn2:assignment>
+      </bpmn2:dataInputAssociation>
+    </bpmn2:task>
+
+    <bpmn2:parallelGateway id="_Join" gatewayDirection="Converging">
+      <bpmn2:incoming>_f5a</bpmn2:incoming>
+      <bpmn2:incoming>_f5b</bpmn2:incoming>
+      <bpmn2:outgoing>_f6</bpmn2:outgoing>
+    </bpmn2:parallelGateway>
+
+    <bpmn2:endEvent id="_End">
+      <bpmn2:incoming>_f6</bpmn2:incoming>
+    </bpmn2:endEvent>
+
+    <bpmn2:sequenceFlow id="_f1" sourceRef="_Start" targetRef="_Timer" />
+    <bpmn2:sequenceFlow id="_f2" sourceRef="_Timer" targetRef="_Split" />
+    <bpmn2:sequenceFlow id="_f3" sourceRef="_Split" targetRef="_Branch1" />
+    <bpmn2:sequenceFlow id="_f4" sourceRef="_Split" targetRef="_Branch2" />
+    <bpmn2:sequenceFlow id="_f5a" sourceRef="_Branch1" targetRef="_Join" />
+    <bpmn2:sequenceFlow id="_f5b" sourceRef="_Branch2" targetRef="_Join" />
+    <bpmn2:sequenceFlow id="_f6" sourceRef="_Join" targetRef="_End" />
+  </bpmn2:process>
+  <bpmndi:BPMNDiagram>
+    <bpmndi:BPMNPlane bpmnElement="asyncsplit">
+      <bpmndi:BPMNShape id="shape__Start" bpmnElement="_Start">
+        <dc:Bounds height="56" width="56" x="100" y="140"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__Timer" bpmnElement="_Timer">
+        <dc:Bounds height="102" width="154" x="200" y="117"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__Split" bpmnElement="_Split">
+        <dc:Bounds height="56" width="56" x="400" y="140"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__Branch1" bpmnElement="_Branch1">
+        <dc:Bounds height="102" width="154" x="500" y="70"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__Branch2" bpmnElement="_Branch2">
+        <dc:Bounds height="102" width="154" x="500" y="210"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__Join" bpmnElement="_Join">
+        <dc:Bounds height="56" width="56" x="700" y="140"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="shape__End" bpmnElement="_End">
+        <dc:Bounds height="56" width="56" x="800" y="140"/>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge id="edge__f1" bpmnElement="_f1">
+        <di:waypoint x="156" y="168"/>
+        <di:waypoint x="200" y="168"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f2" bpmnElement="_f2">
+        <di:waypoint x="354" y="168"/>
+        <di:waypoint x="400" y="168"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f3" bpmnElement="_f3">
+        <di:waypoint x="428" y="140"/>
+        <di:waypoint x="428" y="121"/>
+        <di:waypoint x="500" y="121"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f4" bpmnElement="_f4">
+        <di:waypoint x="428" y="196"/>
+        <di:waypoint x="428" y="261"/>
+        <di:waypoint x="500" y="261"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f5a" bpmnElement="_f5a">
+        <di:waypoint x="654" y="121"/>
+        <di:waypoint x="728" y="121"/>
+        <di:waypoint x="728" y="140"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f5b" bpmnElement="_f5b">
+        <di:waypoint x="654" y="261"/>
+        <di:waypoint x="728" y="261"/>
+        <di:waypoint x="728" y="196"/>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge id="edge__f6" bpmnElement="_f6">
+        <di:waypoint x="756" y="168"/>
+        <di:waypoint x="800" y="168"/>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+  <bpmn2:relationship type="BPSimData">
+    <bpmn2:extensionElements>
+      <bpsim:BPSimData>
+        <bpsim:Scenario id="default" name="Simulationscenario">
+          <bpsim:ScenarioParameters/>
+        </bpsim:Scenario>
+      </bpsim:BPSimData>
+    </bpmn2:extensionElements>
+    <bpmn2:source>_asyncsplit</bpmn2:source>
+    <bpmn2:target>_asyncsplit</bpmn2:target>
+  </bpmn2:relationship>
+</bpmn2:definitions>
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/test/java/org/kie/kogito/it/OptimisticLockingTest.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/test/java/org/kie/kogito/it/OptimisticLockingTest.java
index 04028e27898..ef411737b05 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/test/java/org/kie/kogito/it/OptimisticLockingTest.java
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-processes-persistence-common/src/test/java/org/kie/kogito/it/OptimisticLockingTest.java
@@ -18,6 +18,8 @@
  */
 package org.kie.kogito.it;
 
+import java.util.Map;
+
 import org.junit.jupiter.api.Test;
 
 import io.quarkus.test.junit.TestProfile;
@@ -27,6 +29,7 @@ import static io.restassured.RestAssured.given;
 import static org.awaitility.Awaitility.await;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.equalTo;
 
 @TestProfile(OptimisticLockingProfile.class)
 public abstract class OptimisticLockingTest extends PersistenceTest {
@@ -50,4 +53,55 @@ public abstract class OptimisticLockingTest extends 
PersistenceTest {
                         .then()
                         .statusCode(404));
     }
+
+    /**
+     * Verifies that a process with an async parallel split completes its 
parallel branches
+     * exactly once each, without a ProcessInstanceOptimisticLockingException 
caused by a
+     * stale optimistic-lock version read under concurrent lock access.
+     * <p>
+     * The asyncsplit process: async TimerReplacement → parallel split → async 
Branch1 +
+     * async Branch2 (both sleep {@code waitTime} ms and increment a 
non-transactional counter)
+     * → parallel join → end.
+     * <p>
+     * With {@code waitTime=100} both worker threads complete nearly 
simultaneously, triggering
+     * the race. Before the fix, the second thread would read a stale 
optimistic-lock version
+     * and fail with a locking exception, causing a retry that increments the 
counter a third
+     * time. After the fix the lock is deferred until after the transaction 
commits, so each
+     * branch runs exactly once and the counter equals 2.
+     * <p>
+     * The counter is non-transactional (an AtomicInteger in a @Singleton CDI 
bean) so it
+     * reflects every execution attempt, including those rolled back by a 
failed transaction.
+     */
+    @Test
+    void testAsyncSplitWithParallelAsyncTasks() {
+        // Reset the non-transactional counter before the test run.
+        given().when().delete("/branch-counter").then().statusCode(204);
+
+        final String pid = given().contentType(ContentType.JSON)
+                .when()
+                .body(Map.of("waitTime", 100L))
+                .post("/asyncsplit")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .extract()
+                .path("id");
+
+        // Wait for the process to complete (it ends automatically after the 
parallel join).
+        await().atMost(TIMEOUT)
+                .untilAsserted(() -> given().contentType(ContentType.JSON)
+                        .when()
+                        .get("/asyncsplit/{pid}", pid)
+                        .then()
+                        .statusCode(404));
+
+        // Counter must be exactly 2: Branch1 ran once and Branch2 ran once.
+        // A value > 2 means a retry occurred due to the stale-lock bug.
+        given().when()
+                .get("/branch-counter")
+                .then()
+                .statusCode(200)
+                .body(equalTo("2"));
+    }
 }
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-quarkus-processes-jdbc/pom.xml
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-quarkus-processes-jdbc/pom.xml
index e09843804df..327a56c0acc 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-quarkus-processes-jdbc/pom.xml
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/integration-tests-quarkus-processes-jdbc/pom.xml
@@ -51,6 +51,14 @@
       <groupId>org.kie</groupId>
       <artifactId>kie-addons-quarkus-persistence-jdbc</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.kie</groupId>
+      <artifactId>kogito-addons-quarkus-embedded-jobs</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.kie</groupId>
+      <artifactId>kogito-addons-quarkus-embedded-jobs-jpa</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.kie</groupId>
       <artifactId>kie-addons-quarkus-process-management</artifactId>
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/pom.xml
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/pom.xml
index 51ffc80caad..7914f5f431c 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/pom.xml
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes-persistence/pom.xml
@@ -29,6 +29,23 @@
   <artifactId>integration-tests-quarkus-processes-persistence</artifactId>
   <name>Kogito :: Integration Tests :: Quarkus :: Processes :: 
Persistence</name>
 
+  <properties>
+    
<allowedPomsList>org.kie.kogito:kogito-quarkus-bom,org.kie.kogito:integration-tests-quarkus-processes-persistence</allowedPomsList>
+    <allowed_GA>org.kie.kogito:kogito-apps-quarkus-bom</allowed_GA>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.kie.kogito</groupId>
+        <artifactId>kogito-apps-quarkus-bom</artifactId>
+        <version>${project.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <modules>
     <module>integration-tests-processes-persistence-common</module>
     <module>integration-tests-quarkus-processes-postgresql</module>
diff --git 
a/kogito-springboot/addons/persistence/jdbc/src/main/java/org/kie/kogito/persistence/springboot/JDBCProcessInstancesFactory.java
 
b/kogito-springboot/addons/persistence/jdbc/src/main/java/org/kie/kogito/persistence/springboot/JDBCProcessInstancesFactory.java
index 0cf18a58d2e..0d9119d33cf 100644
--- 
a/kogito-springboot/addons/persistence/jdbc/src/main/java/org/kie/kogito/persistence/springboot/JDBCProcessInstancesFactory.java
+++ 
b/kogito-springboot/addons/persistence/jdbc/src/main/java/org/kie/kogito/persistence/springboot/JDBCProcessInstancesFactory.java
@@ -24,12 +24,16 @@ import javax.sql.DataSource;
 
 import org.kie.kogito.internal.process.runtime.HeadersPersistentConfig;
 import org.kie.kogito.persistence.jdbc.AbstractProcessInstancesFactory;
+import org.kie.kogito.persistence.jdbc.JDBCProcessInstances;
+import org.kie.kogito.process.Process;
 import org.kie.kogito.process.Processes;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
 import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 
 @Component
 public class JDBCProcessInstancesFactory extends 
AbstractProcessInstancesFactory {
@@ -49,4 +53,22 @@ public class JDBCProcessInstancesFactory extends 
AbstractProcessInstancesFactory
         super(new TransactionAwareDataSourceProxy(dataSource), lock, new 
HeadersPersistentConfig(headersEnabled, headersExcluded), dataIsolationEnabled 
? processes : null);
     }
 
+    @Override
+    public JDBCProcessInstances<?> createProcessInstances(Process<?> process) {
+        JDBCProcessInstances<?> instances = 
super.createProcessInstances(process);
+        instances.setTransactionRegistrar(unlockAction -> {
+            if (TransactionSynchronizationManager.isSynchronizationActive()) {
+                TransactionSynchronizationManager.registerSynchronization(new 
TransactionSynchronization() {
+                    @Override
+                    public void afterCompletion(int status) {
+                        unlockAction.run();
+                    }
+                });
+            } else {
+                unlockAction.run();
+            }
+        });
+        return instances;
+    }
+
 }
diff --git 
a/kogito-springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java
 
b/kogito-springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java
index 7814bfe5236..35c4bbaf1b1 100644
--- 
a/kogito-springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java
+++ 
b/kogito-springboot/starters/kogito-processes-spring-boot-starter/src/main/java/org/kie/kogito/process/KogitoBeanProducer.java
@@ -90,7 +90,13 @@ public class KogitoBeanProducer {
 
     @Bean
     @ConditionalOnMissingBean(Vertx.class)
-    Vertx vertx() {
+    Vertx vertxMutiny() {
         return Vertx.vertx();
     }
+
+    @Bean
+    @ConditionalOnMissingBean(io.vertx.core.Vertx.class)
+    io.vertx.core.Vertx vertx() {
+        return io.vertx.core.Vertx.vertx();
+    }
 }


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

Reply via email to