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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new d775c350c7b CAUSEWAY-3937: GridCache to support recursive update
d775c350c7b is described below

commit d775c350c7bbf52a0cc93edcd7b5579760617ede
Author: Andi Huber <[email protected]>
AuthorDate: Sun Nov 2 08:48:51 2025 +0100

    CAUSEWAY-3937: GridCache to support recursive update
    
    should also fix random test failures
---
 .../core/metamodel/services/grid/GridCache.java    | 66 ++++++++++++++++++++--
 1 file changed, 62 insertions(+), 4 deletions(-)

diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
index 35813f48404..e566bb76a12 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
-import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
@@ -35,21 +34,80 @@
  */
 @Slf4j
 record GridCache(
-    Map<LayoutKey, Try<BSGrid>> gridsByKey) {
+    ConcurrentMapWrapper<LayoutKey, Try<BSGrid>> gridsByKey) {
 
     public GridCache(final GridLoadingContext gridLoadingContext) {
-        this(new ConcurrentHashMap<>());
+        this(new ConcurrentMapWrapper<>(new ConcurrentHashMap<>()));
     }
 
     /**
      * To support metamodel invalidation/rebuilding of spec.
      */
     public void remove(final Class<?> domainClass) {
-        
gridsByKey.entrySet().removeIf(entry->entry.getKey().domainClass().equals(domainClass));
+        
gridsByKey.map().entrySet().removeIf(entry->entry.getKey().domainClass().equals(domainClass));
     }
 
     public Try<BSGrid> computeIfAbsent(final LayoutKey layoutKey, final 
Function<LayoutKey, Try<BSGrid>> factory) {
         return gridsByKey.computeIfAbsent(layoutKey, factory);
     }
 
+    /* when using ConcurrentHashMap directly we may see
+    java.lang.IllegalStateException: Recursive update
+    at 
java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1779)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridCache.computeIfAbsent(GridCache.java:52)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridServiceDefault.load(GridServiceDefault.java:95)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.load(BSGridFacet.java:117)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.lambda$normalized$0(BSGridFacet.java:86)
+    at 
java.base/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1932)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.normalized(BSGridFacet.java:82)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.getGrid(BSGridFacet.java:68)
+    at 
org.apache.causeway.core.metamodel.util.Facets.lambda$gridPreload$0(Facets.java:204)
+    at java.base/java.util.Optional.ifPresent(Optional.java:178)
+    at 
org.apache.causeway.core.metamodel.util.Facets.gridPreload(Facets.java:200)
+    at 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationDefault.introspectFully(ObjectSpecificationDefault.java:640)
+    at 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationDefault.introspectUpTo(ObjectSpecificationDefault.java:615)
+    at 
org.apache.causeway.core.metamodel.spec.impl.ObjectSpecificationDefault.streamDeclaredAssociations(ObjectSpecificationDefault.java:1000)
+    at 
org.apache.causeway.core.metamodel.spec.impl.ObjectMemberContainer.streamAssociations(ObjectMemberContainer.java:127)
+    at 
org.apache.causeway.core.metamodel.spec.impl.ObjectMemberContainer.streamAssociations(ObjectMemberContainer.java:128)
+    at 
org.apache.causeway.core.metamodel.spec.feature.ObjectAssociationContainer.streamProperties(ObjectAssociationContainer.java:118)
+    at 
org.apache.causeway.core.metamodel.services.grid.ObjectMemberResolverForGrid.validateAndNormalize(ObjectMemberResolverForGrid.java:148)
+    at 
org.apache.causeway.core.metamodel.services.grid.ObjectMemberResolverForGrid.resolve(ObjectMemberResolverForGrid.java:115)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridServiceDefault.lambda$tryLoadNoCache$7(GridServiceDefault.java:110)
+    at java.base/java.util.Optional.orElseGet(Optional.java:364)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridServiceDefault.lambda$tryLoadNoCache$6(GridServiceDefault.java:110)
+    at 
org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:63)
+    at 
org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:51)
+    ..
+    at 
org.apache.causeway.core.metamodel.services.grid.GridServiceDefault.tryLoadNoCache(GridServiceDefault.java:110)
+    at 
java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1724)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridCache.computeIfAbsent(GridCache.java:52)
+    at 
org.apache.causeway.core.metamodel.services.grid.GridServiceDefault.load(GridServiceDefault.java:95)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.load(BSGridFacet.java:117)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.lambda$normalized$0(BSGridFacet.java:86)
+    at 
java.base/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1932)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.normalized(BSGridFacet.java:82)
+    at 
org.apache.causeway.core.metamodel.facets.object.grid.BSGridFacet.getGrid(BSGridFacet.java:68)
+    ..
+    at 
org.apache.causeway.core.metamodel.util.Facets.gridPreload(Facets.java:200)*/
+    record ConcurrentMapWrapper<K, V>(ConcurrentHashMap<K, V> map) {
+
+        public V computeIfAbsent(final K key, final 
java.util.function.Function<? super K, ? extends V> mappingFunction) {
+            // Use an atomic flag to track whether a recursive call is 
currently happening.
+            // It ensures that multiple threads can manage their recursion 
state independently.
+            final ThreadLocal<Boolean> isRecursing = 
ThreadLocal.withInitial(() -> false);
+
+            // Define a recursive compute function
+            return map.compute(key, (k, v) -> {
+                if (isRecursing.get()) return v; // return current value if 
already in recursion
+
+                isRecursing.set(true); // mark as in recursion
+                try {
+                    return v == null ? mappingFunction.apply(k) : v;
+                } finally {
+                    isRecursing.remove(); // clean up the flag variable
+                }
+            });
+        }
+    }
+
 }

Reply via email to