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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3ba8dca0c3 [incubator-kie-issues#2339] NPE in `JDBCProcessInstances.` 
when retriggering `CallActivity` nodes with `waitForCompletion=true` (#4315)
3ba8dca0c3 is described below

commit 3ba8dca0c3c9678dc4cca404292b5d4d74874e7e
Author: Pere Fernández <[email protected]>
AuthorDate: Fri Jun 12 10:56:31 2026 +0200

    [incubator-kie-issues#2339] NPE in `JDBCProcessInstances.` when 
retriggering `CallActivity` nodes with `waitForCompletion=true` (#4315)
---
 .../process/impl/AbstractProcessInstance.java      | 12 ++-
 .../ContextAwareProcessInstanceLockStrategy.java   |  5 ++
 .../lock/ProcessInstanceAtomicLockStrategy.java    |  9 ++-
 .../impl/lock/ProcessInstanceLockStrategy.java     |  8 ++
 .../ProcessInstanceAtomicLockStrategyTest.java     | 87 ++++++++++++++++++++++
 .../src/test/java/org/jbpm/bpmn2/ActivityTest.java | 47 ++++++++++++
 6 files changed, 164 insertions(+), 4 deletions(-)

diff --git 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
index d00495594c..842fdb08ff 100644
--- 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
+++ 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/AbstractProcessInstance.java
@@ -612,6 +612,9 @@ 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
+        boolean isReentrant = 
processInstanceLockStrategy.isLockedByCurrentThread(id);
+
         return processInstanceLockStrategy.executeOperation(id, () -> {
             WorkflowProcessInstanceImpl workflowProcessInstance = 
internalLoadProcessInstanceState();
             if (isProcessInstanceConnected()) {
@@ -621,7 +624,7 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
             try {
                 outcome = execution.apply(workflowProcessInstance);
             } catch (Throwable th) {
-                // clean up after non expected error
+                // clean up after non-expected error
                 if (isProcessInstanceConnected()) {
                     
getProcessRuntime().getProcessInstanceManager().removeProcessInstance(workflowProcessInstance);
                 }
@@ -636,7 +639,12 @@ public abstract class AbstractProcessInstance<T extends 
Model> implements Proces
                 syncPersistence(workflowProcessInstance);
                 
getProcessRuntime().getProcessInstanceManager().removeProcessInstance(workflowProcessInstance);
             }
-            internalUnloadProcessInstanceState();
+
+            // Only unload state if this is not a reentrant call, this 
prevents nested calls from unloading the parent's state
+            if (!isReentrant) {
+                internalUnloadProcessInstanceState();
+            }
+
             return outcome;
         });
     }
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
index 9d656f1d5e..eabf6a15e1 100644
--- 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
+++ 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ContextAwareProcessInstanceLockStrategy.java
@@ -66,4 +66,9 @@ public class ContextAwareProcessInstanceLockStrategy 
implements ProcessInstanceL
             }
         });
     }
+
+    @Override
+    public boolean isLockedByCurrentThread(String processInstanceId) {
+        return delegate.isLockedByCurrentThread(processInstanceId);
+    }
 }
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
index ad78e2d581..0cd3a27531 100644
--- 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
+++ 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategy.java
@@ -84,8 +84,7 @@ public class ProcessInstanceAtomicLockStrategy implements 
ProcessInstanceLockStr
             return newHolder;
         });
 
-        // at this points this is a safe ask as if we invoked prior to this 
point the hold it will always return
-        // properly
+        // At this point this is a safe ask as if we invoked prior to this 
point the hold it will always return properly
         boolean alreadyAcquired = 
processInstanceLockHolder.isHeldByCurrentThread();
         try {
             if (!alreadyAcquired) {
@@ -116,6 +115,12 @@ public class ProcessInstanceAtomicLockStrategy implements 
ProcessInstanceLockStr
 
     }
 
+    @Override
+    public boolean isLockedByCurrentThread(String processInstanceId) {
+        ProcessInstanceLockHolder holder = locks.get(processInstanceId);
+        return holder != null && holder.isHeldByCurrentThread();
+    }
+
     public static synchronized ProcessInstanceLockStrategy instance() {
         if (INSTANCE == null) {
             INSTANCE = new ProcessInstanceAtomicLockStrategy();
diff --git 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
index 02f8350866..5d0a27d5aa 100644
--- 
a/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
+++ 
b/jbpm/jbpm-flow/src/main/java/org/kie/kogito/process/impl/lock/ProcessInstanceLockStrategy.java
@@ -22,4 +22,12 @@ public interface ProcessInstanceLockStrategy {
 
     <T> T executeOperation(String processInstanceId, WorkflowAtomicExecutor<T> 
empty);
 
+    /**
+     * Checks if the current thread already holds the lock for the given 
process instance.
+     *
+     * @param processInstanceId the process instance id
+     * @return true if the current thread already holds the lock, false 
otherwise
+     */
+    boolean isLockedByCurrentThread(String processInstanceId);
+
 }
diff --git 
a/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategyTest.java
 
b/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategyTest.java
new file mode 100644
index 0000000000..909968ad3a
--- /dev/null
+++ 
b/jbpm/jbpm-flow/src/test/java/org/kie/kogito/process/impl/lock/ProcessInstanceAtomicLockStrategyTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.process.impl.lock;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ProcessInstanceAtomicLockStrategyTest {
+
+    private ProcessInstanceAtomicLockStrategy strategy;
+    private static final String PROCESS_INSTANCE_ID = 
"testProcessInstanceId-1";
+
+    @BeforeEach
+    void setUp() {
+        strategy = (ProcessInstanceAtomicLockStrategy) 
ProcessInstanceAtomicLockStrategy.instance();
+    }
+
+    @Test
+    void testProcessInstanceIsNotLockedByCurrentThread() {
+        boolean isLocked = 
strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID);
+
+        assertThat(isLocked).isFalse();
+    }
+
+    @Test
+    void testProcessInstanceIsLockedByCurrentThread() {
+        strategy.executeOperation(PROCESS_INSTANCE_ID, () -> {
+            boolean isLocked = 
strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID);
+            assertThat(isLocked).isTrue();
+            return null;
+        });
+    }
+
+    @Test
+    void testProcessInstanceIsLockedByCurrentThreadReentrantCalls() {
+
+        // Verifying that process instance is locked in nested operations
+        strategy.executeOperation(PROCESS_INSTANCE_ID, () -> {
+
+            
assertThat(strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID)).isTrue();
+
+            return strategy.executeOperation(PROCESS_INSTANCE_ID, () -> {
+
+                
assertThat(strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID)).isTrue();
+
+                return strategy.executeOperation(PROCESS_INSTANCE_ID, () -> {
+                    
assertThat(strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID)).isTrue();
+                    return "success";
+                });
+            });
+        });
+
+        // After all operations complete, lock should be released
+        
assertThat(strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID)).isFalse();
+    }
+
+    @Test
+    void testDifferentProcessInstancesIsLockedByCurrentThread() {
+        String processInstanceId2 = "testProcessInstanceId-2";
+
+        strategy.executeOperation(PROCESS_INSTANCE_ID, () -> {
+            // verify that only testProcessInstanceId-1 is locked
+            
assertThat(strategy.isLockedByCurrentThread(PROCESS_INSTANCE_ID)).isTrue();
+
+            
assertThat(strategy.isLockedByCurrentThread(processInstanceId2)).isFalse();
+            return null;
+        });
+    }
+}
diff --git a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ActivityTest.java 
b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ActivityTest.java
index 44d6a424f4..69bb990bf0 100755
--- a/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ActivityTest.java
+++ b/jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/ActivityTest.java
@@ -1803,6 +1803,53 @@ public class ActivityTest extends JbpmBpmn2TestCase {
         
assertThat(instance).extracting(ProcessInstance::status).isEqualTo(org.kie.kogito.process.ProcessInstance.STATE_ABORTED);
     }
 
+    @Test
+    public void testRetriggerCallActivityNodeInstance() {
+        final String flow1NodeUniqueId = 
"_B45422B2-F0D4-40D2-A2BF-72BE1F23EC81";
+
+        Application app = ProcessTestHelper.newApplication();
+        ProcessTestHelper.registerHandler(app, "Human Task", new 
DefaultKogitoWorkItemHandler());
+        org.kie.kogito.process.Process<FlowMainModel> main = 
FlowMainProcess.newProcess(app);
+        org.kie.kogito.process.Process<FlowChild1Model> child1 = 
FlowChild1Process.newProcess(app);
+        FlowChild2Process.newProcess(app);
+        FlowChild3Process.newProcess(app);
+
+        org.kie.kogito.process.ProcessInstance<FlowMainModel> instance = 
main.createInstance(main.createModel());
+        instance.start();
+
+        assertThat(child1.instances().stream().count()).isEqualTo(1);
+
+        Collection<KogitoNodeInstance> childNodeInstances = 
instance.findNodes(kogitoNodeInstance -> 
kogitoNodeInstance.getNodeDefinitionId().equals(flow1NodeUniqueId));
+
+        assertThat(childNodeInstances).hasSize(1);
+
+        KogitoNodeInstance flow1NodeInstance = 
childNodeInstances.iterator().next();
+
+        assertThat(flow1NodeInstance)
+                .isNotNull()
+                .hasFieldOrProperty("id")
+                .hasFieldOrPropertyWithValue("isRetrigger", false);
+
+        instance.retriggerNodeInstance(flow1NodeInstance.getId());
+
+        Collection<KogitoNodeInstance> childNodeInstancesAfterRetrigger = 
instance.findNodes(kogitoNodeInstance -> 
kogitoNodeInstance.getNodeDefinitionId().equals(flow1NodeUniqueId));
+
+        assertThat(childNodeInstancesAfterRetrigger).hasSize(1);
+
+        KogitoNodeInstance retriggeredFlow1NodeInstance = 
childNodeInstancesAfterRetrigger
+                .iterator()
+                .next();
+
+        assertThat(retriggeredFlow1NodeInstance)
+                .isNotNull()
+                .hasFieldOrPropertyWithValue("isRetrigger", true)
+                .hasFieldOrProperty("id")
+                .extracting("id")
+                .isNotEqualTo(flow1NodeInstance.getId());
+
+        instance.abort();
+    }
+
     @Test
     public void testAbortChildProcessUponCompletion() {
         Application app = ProcessTestHelper.newApplication();


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

Reply via email to