Copilot commented on code in PR #2377:
URL: https://github.com/apache/groovy/pull/2377#discussion_r2766032370


##########
src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java:
##########
@@ -123,6 +123,18 @@ public void resetFallbackCount() {
         fallbackRound.incrementAndGet();
     }
 
+    /**
+     * Clear the LRU cache and reset fallback count.
+     * Called when metaclass changes to ensure stale method handles are 
discarded.
+     */
+    public void clearCache() {
+        synchronized (lruCache) {
+            lruCache.clear();
+        }
+        latestHitMethodHandleWrapperSoftReference = null;

Review Comment:
   The `clearCache()` method has a potential race condition with the 
`latestHitMethodHandleWrapperSoftReference` field. The field is declared as 
`volatile` (line 48) and is accessed outside the synchronized block (line 134). 
This creates a race condition where:
   
   1. Thread A could be in `getAndPut()` reading 
`latestHitMethodHandleWrapperSoftReference` at line 84
   2. Thread B calls `clearCache()` and sets 
`latestHitMethodHandleWrapperSoftReference = null` at line 134
   3. Thread A proceeds with stale data and might write back the reference at 
line 92
   
   The assignment to `latestHitMethodHandleWrapperSoftReference` should be done 
inside the synchronized block to ensure atomicity with the cache clearing 
operation.
   ```suggestion
               latestHitMethodHandleWrapperSoftReference = null;
           }
   ```



##########
src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java:
##########
@@ -181,24 +189,54 @@ public int getOrderNumber() {
     }
 
     protected static SwitchPoint switchPoint = new SwitchPoint();
+    
+    /**
+     * Weak set of all CacheableCallSites. Used to invalidate caches when 
metaclass changes.
+     * Uses WeakReferences so call sites can be garbage collected when no 
longer referenced.
+     */
+    private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES 
= ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);
 
     static {
         
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
     }
+    
+    /**
+     * Register a call site for cache invalidation when metaclass changes.
+     */
+    static void registerCallSite(CacheableCallSite callSite) {
+        ALL_CALL_SITES.add(new WeakReference<>(callSite));

Review Comment:
   The WeakReference cleanup relies entirely on `invalidateSwitchPoints()` 
being called to remove garbage-collected references. If metaclass changes are 
infrequent or never occur in a long-running application, the `ALL_CALL_SITES` 
set could accumulate a large number of cleared WeakReferences (where `ref.get() 
== null`), consuming memory unnecessarily.
   
   Consider adding a periodic cleanup mechanism or implementing cleanup on 
registration if the set size exceeds a threshold. For example, you could check 
the set size in `registerCallSite()` and trigger cleanup if it exceeds a 
configurable threshold, similar to how other Groovy components handle weak 
reference cleanup.
   ```suggestion
   
       /**
        * Threshold for triggering cleanup of cleared WeakReferences from 
ALL_CALL_SITES.
        * Configurable via system property 
"groovy.indy.callsites.cleanup.threshold".
        */
       private static final long ALL_CALL_SITES_CLEANUP_THRESHOLD =
               
SystemUtil.getLongSafe("groovy.indy.callsites.cleanup.threshold", 10_000L);
   
       static {
           
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
       }
       
       /**
        * Remove entries from ALL_CALL_SITES whose referents have been 
garbage-collected.
        */
       private static void cleanupClearedCallSites() {
           ALL_CALL_SITES.removeIf(ref -> ref.get() == null);
       }
       
       /**
        * Register a call site for cache invalidation when metaclass changes.
        */
       static void registerCallSite(CacheableCallSite callSite) {
           ALL_CALL_SITES.add(new WeakReference<>(callSite));
           // Periodically clean up cleared WeakReferences to avoid unbounded 
growth
           if (ALL_CALL_SITES.size() > ALL_CALL_SITES_CLEANUP_THRESHOLD) {
               cleanupClearedCallSites();
           }
   ```



##########
src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java:
##########
@@ -181,24 +189,54 @@ public int getOrderNumber() {
     }
 
     protected static SwitchPoint switchPoint = new SwitchPoint();
+    
+    /**
+     * Weak set of all CacheableCallSites. Used to invalidate caches when 
metaclass changes.
+     * Uses WeakReferences so call sites can be garbage collected when no 
longer referenced.
+     */
+    private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES 
= ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);
 
     static {
         
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu
 -> invalidateSwitchPoints());
     }
+    
+    /**
+     * Register a call site for cache invalidation when metaclass changes.
+     */
+    static void registerCallSite(CacheableCallSite callSite) {
+        ALL_CALL_SITES.add(new WeakReference<>(callSite));
+    }
 
     /**
-     * Callback for constant metaclass update change
+     * Callback for constant metaclass update change.
+     * Invalidates all call site caches to ensure metaclass changes are 
visible.
      */
     protected static void invalidateSwitchPoints() {
         if (LOG_ENABLED) {
-            LOG.info("invalidating switch point");
+            LOG.info("invalidating switch point and call site caches");
         }
 
         synchronized (IndyInterface.class) {
             SwitchPoint old = switchPoint;
             switchPoint = new SwitchPoint();
             SwitchPoint.invalidateAll(new SwitchPoint[]{old});
         }
+        
+        // Invalidate all call site caches and reset targets to default (cache 
lookup).
+        ALL_CALL_SITES.removeIf(ref -> {
+            CacheableCallSite cs = ref.get();
+            if (cs == null) {
+                return true; // Remove garbage collected references
+            }
+            // Reset target to default (fromCache) so next call goes through 
cache lookup
+            MethodHandle defaultTarget = cs.getDefaultTarget();
+            if (defaultTarget != null && cs.getTarget() != defaultTarget) {
+                cs.setTarget(defaultTarget);

Review Comment:
   The `invalidateSwitchPoints()` method has a potential race condition when 
resetting call site targets. The method reads the defaultTarget and 
currentTarget, then conditionally sets the target, all without synchronization 
on the CacheableCallSite object. This creates a race where:
   
   1. Thread A could be calling `setTarget()` on the call site (e.g., during 
optimization)
   2. Thread B (invalidation) reads `cs.getTarget()` and 
`cs.getDefaultTarget()` 
   3. Thread A completes its `setTarget()` call
   4. Thread B then calls `cs.setTarget(defaultTarget)`, potentially 
overwriting Thread A's update
   
   Since `MutableCallSite.setTarget()` is synchronized on the call site itself, 
consider synchronizing on the call site object when checking and resetting the 
target to ensure atomicity.
   ```suggestion
               synchronized (cs) {
                   MethodHandle defaultTarget = cs.getDefaultTarget();
                   MethodHandle currentTarget = cs.getTarget();
                   if (defaultTarget != null && currentTarget != defaultTarget) 
{
                       cs.setTarget(defaultTarget);
                   }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to