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

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


The following commit(s) were added to refs/heads/main by this push:
     new d617a0dcec [incubator-kie-drools-6445] Problem in calling 
RuleUnitProviderImpl l… (#6520)
d617a0dcec is described below

commit d617a0dcecd768fd88f0e707da7853ad0332ada5
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Fri Nov 21 19:05:09 2025 +0900

    [incubator-kie-drools-6445] Problem in calling RuleUnitProviderImpl l… 
(#6520)
    
    * [incubator-kie-drools-6445] Problem in calling RuleUnitProviderImpl 
loadRuleUnits from a multithreaded context
    
    * Add comments for double-get-check and suppress warning for unchecked cast
    
    * replace synchronized with ReentrantLock
---
 .../ruleunits/impl/RuleUnitProviderImpl.java       | 60 +++++++++++-----
 .../impl/RuleUnitProviderImplConcurrencyTest.java  | 80 ++++++++++++++++++++++
 drools-ruleunits/pom.xml                           | 28 ++++++++
 3 files changed, 150 insertions(+), 18 deletions(-)

diff --git 
a/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/RuleUnitProviderImpl.java
 
b/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/RuleUnitProviderImpl.java
index e464ed95d8..1c38af11a4 100644
--- 
a/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/RuleUnitProviderImpl.java
+++ 
b/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/RuleUnitProviderImpl.java
@@ -32,6 +32,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.stream.Collectors;
@@ -66,21 +70,36 @@ public class RuleUnitProviderImpl implements 
RuleUnitProvider {
 
     private static final boolean USE_EXEC_MODEL = true;
 
-    private final Map<String, RuleUnit> ruleUnitMap;
+    private final ConcurrentMap<String, RuleUnit> ruleUnitMap;
+    private final Lock generationLock = new ReentrantLock();
 
     public RuleUnitProviderImpl() {
-        this.ruleUnitMap = 
loadRuleUnits(Thread.currentThread().getContextClassLoader());
+        this.ruleUnitMap = new 
ConcurrentHashMap<>(loadRuleUnits(Thread.currentThread().getContextClassLoader()));
     }
 
     @Override
+    @SuppressWarnings("unchecked") // safe because ruleUnitMap keys are the 
canonical names of their matching RuleUnitData types
     public <T extends RuleUnitData> RuleUnit<T> getRuleUnit(T ruleUnitData) {
         String ruleUnitName = getRuleUnitName(ruleUnitData);
-        RuleUnit<T> ruleUnit = ruleUnitMap.get(ruleUnitName);
+        RuleUnit<T> ruleUnit = (RuleUnit<T>) ruleUnitMap.get(ruleUnitName);
         if (ruleUnit != null) {
             return ruleUnit;
         }
-        ruleUnitMap.putAll(generateRuleUnit(ruleUnitData));
-        return ruleUnitMap.get(ruleUnitName);
+        // double-check inside the lock so concurrent cache misses don't 
re-generate the same unit twice.
+        // computeIfAbsent cannot express this because invalidateRuleUnits may 
remove entries while another thread is computing;
+        // we need the whole read-check-generate-publish sequence under one 
lock.
+        generationLock.lock();
+        try {
+            ruleUnit = (RuleUnit<T>) ruleUnitMap.get(ruleUnitName);
+            if (ruleUnit == null) {
+                Map<String, RuleUnit> generated = 
generateRuleUnit(ruleUnitData);
+                ruleUnitMap.putAll(generated);
+                ruleUnit = (RuleUnit<T>) generated.get(ruleUnitName);
+            }
+        } finally {
+            generationLock.unlock();
+        }
+        return ruleUnit;
     }
 
     protected <T extends RuleUnitData> Map<String, RuleUnit> 
generateRuleUnit(T ruleUnitData) {
@@ -249,19 +268,24 @@ public class RuleUnitProviderImpl implements 
RuleUnitProvider {
 
     @Override
     public <T extends RuleUnitData> int invalidateRuleUnits(Class<T> 
ruleUnitDataClass) {
-        if (NamedRuleUnitData.class.isAssignableFrom(ruleUnitDataClass)) {
-            // NamedRuleUnitData may create multiple RuleUnits
-            List<String> invalidateKeys = ruleUnitMap.entrySet()
-                    .stream()
-                    .filter(entry -> 
hasSameRuleUnitDataClass(entry.getValue(), ruleUnitDataClass))
-                    .map(Map.Entry::getKey)
-                    .collect(Collectors.toList());
-            invalidateKeys.forEach(ruleUnitMap::remove);
-            return invalidateKeys.size();
-        } else {
-            String ruleUnitName = getRuleUnitName(ruleUnitDataClass);
-            RuleUnit remove = ruleUnitMap.remove(ruleUnitName);
-            return remove == null ? 0 : 1;
+        generationLock.lock();
+        try {
+            if (NamedRuleUnitData.class.isAssignableFrom(ruleUnitDataClass)) {
+                // NamedRuleUnitData may create multiple RuleUnits
+                List<String> invalidateKeys = ruleUnitMap.entrySet()
+                        .stream()
+                        .filter(entry -> 
hasSameRuleUnitDataClass(entry.getValue(), ruleUnitDataClass))
+                        .map(Map.Entry::getKey)
+                        .collect(Collectors.toList());
+                invalidateKeys.forEach(ruleUnitMap::remove);
+                return invalidateKeys.size();
+            } else {
+                String ruleUnitName = getRuleUnitName(ruleUnitDataClass);
+                RuleUnit remove = ruleUnitMap.remove(ruleUnitName);
+                return remove == null ? 0 : 1;
+            }
+        } finally {
+            generationLock.unlock();
         }
     }
 
diff --git 
a/drools-ruleunits/drools-ruleunits-impl/src/test/java/org/drools/ruleunits/impl/RuleUnitProviderImplConcurrencyTest.java
 
b/drools-ruleunits/drools-ruleunits-impl/src/test/java/org/drools/ruleunits/impl/RuleUnitProviderImplConcurrencyTest.java
new file mode 100644
index 0000000000..49d543f8e5
--- /dev/null
+++ 
b/drools-ruleunits/drools-ruleunits-impl/src/test/java/org/drools/ruleunits/impl/RuleUnitProviderImplConcurrencyTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.drools.ruleunits.impl;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.drools.ruleunits.api.RuleUnit;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RuleUnitProviderImplConcurrencyTest {
+
+    @EnabledIfSystemProperty(named = "runTurtleTests", matches = "true")
+    @Test
+    void loadRuleUnitsFromMultipleThreads() throws InterruptedException {
+        // https://github.com/apache/incubator-kie-drools/issues/6445
+        for (int n = 0; n < 100; n++) {
+            loadRuleUnitsFromMultipleThreadsOnce();
+        }
+    }
+
+    private void loadRuleUnitsFromMultipleThreadsOnce() throws 
InterruptedException {
+        RuleUnitProviderImpl provider = new RuleUnitProviderImpl();
+        provider.invalidateRuleUnits(HelloWorldUnit.class);
+
+        int threadCount = 3; // too many threads don't seem to trigger the 
issue more often
+        ExecutorService executorService = 
Executors.newFixedThreadPool(threadCount);
+        CyclicBarrier startBarrier = new CyclicBarrier(threadCount);
+        Queue<Throwable> errors = new ConcurrentLinkedQueue<>();
+
+        try {
+            for (int i = 0; i < threadCount; i++) {
+                executorService.submit(() -> {
+                    try {
+                        startBarrier.await();
+                        RuleUnit<HelloWorldUnit> ruleUnit = 
provider.getRuleUnit(new HelloWorldUnit());
+                        assertThat(ruleUnit).isNotNull();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        errors.add(e);
+                    } catch (Throwable t) {
+                        errors.add(t);
+                    }
+                });
+            }
+
+            executorService.shutdown();
+            assertThat(executorService.awaitTermination(30, 
TimeUnit.SECONDS)).isTrue();
+        } finally {
+            executorService.shutdownNow();
+        }
+
+        assertThat(errors)
+                .withFailMessage(() -> "Unexpected errors while loading rule 
units concurrently: " + errors)
+                .isEmpty();
+    }
+}
diff --git a/drools-ruleunits/pom.xml b/drools-ruleunits/pom.xml
index 587640b825..390d32d634 100644
--- a/drools-ruleunits/pom.xml
+++ b/drools-ruleunits/pom.xml
@@ -59,4 +59,32 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
+
+  <profiles>
+    <profile>
+      <id>runTurtleTests</id>
+      <activation>
+        <property>
+          <name>runTurtleTests</name>
+        </property>
+      </activation>
+      <build>
+        <pluginManagement>
+          <plugins>
+            <plugin>
+              <artifactId>maven-surefire-plugin</artifactId>
+              <configuration>
+                <systemProperties>
+                  <property>
+                    <name>runTurtleTests</name>
+                    <value>true</value>
+                  </property>
+                </systemProperties>
+              </configuration>
+            </plugin>
+          </plugins>
+        </pluginManagement>
+      </build>
+    </profile>
+  </profiles>
 </project>


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

Reply via email to