Skip to content

Commit 6c6593e

Browse files
committed
GROOVY-10307: Address PR review feedback for thread safety and documentation
- Fix race condition in invalidateSwitchPoints(): synchronize target reset and cache clear operations per call site to ensure atomicity - Fix race condition in clearCache(): move latestHitMethodHandleWrapper null assignment inside synchronized block for consistency with getAndPut - Add comprehensive Javadoc for INDY_SWITCHPOINT_GUARD explaining performance implications of enabling the global switchpoint guard - Document WeakReference cleanup behavior in ALL_CALL_SITES and registerCallSite() to clarify garbage collection semantics - Simplify inline comment in setGuards() with detailed Javadoc on field
1 parent ca36b8c commit 6c6593e

File tree

3 files changed

+41
-24
lines changed

3 files changed

+41
-24
lines changed

src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,18 @@ public void setFallbackTarget(MethodHandle fallbackTarget) {
128128
/**
129129
* Clear the cache entirely. Called when metaclass changes to ensure
130130
* stale method handles are discarded.
131+
* <p>
132+
* This method synchronizes on the lruCache to ensure atomicity with
133+
* concurrent {@link #getAndPut} operations.
131134
*/
132135
public void clearCache() {
133-
// Clear the latest hit reference
134-
latestHitMethodHandleWrapperSoftReference = null;
135-
136-
// Clear the LRU cache
136+
// Clear the latest hit reference and the LRU cache atomically
137137
synchronized (lruCache) {
138+
latestHitMethodHandleWrapperSoftReference = null;
138139
lruCache.clear();
139140
}
140141

141-
// Reset fallback count
142+
// Reset fallback count (atomic operation, doesn't need to be in sync block)
142143
fallbackCount.set(0);
143144
}
144145
}

src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,13 @@ public static CallType fromCallSiteName(String callSiteName) {
182182
/**
183183
* Weak set of all CacheableCallSites. Used to invalidate caches when metaclass changes.
184184
* Uses WeakReferences so call sites can be garbage collected when no longer referenced.
185+
* <p>
186+
* Note: Stale (garbage-collected) WeakReferences are cleaned up during each call to
187+
* {@link #invalidateSwitchPoints()}. In applications with infrequent metaclass changes,
188+
* the set may accumulate some dead references between invalidations. This is acceptable
189+
* because: (1) the memory overhead per dead reference is minimal (~16 bytes), and
190+
* (2) frameworks like Grails that benefit most from this optimization have frequent
191+
* metaclass changes that trigger regular cleanup.
185192
*/
186193
private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES = ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);
187194

@@ -191,6 +198,10 @@ public static CallType fromCallSiteName(String callSiteName) {
191198

192199
/**
193200
* Register a call site for cache invalidation when metaclass changes.
201+
* <p>
202+
* Registered call sites are held via WeakReferences and will be automatically
203+
* removed when garbage collected. Cleanup of stale references occurs during
204+
* {@link #invalidateSwitchPoints()}.
194205
*/
195206
static void registerCallSite(CacheableCallSite callSite) {
196207
ALL_CALL_SITES.add(new WeakReference<>(callSite));
@@ -211,20 +222,24 @@ protected static void invalidateSwitchPoints() {
211222
SwitchPoint.invalidateAll(new SwitchPoint[]{old});
212223
}
213224

214-
// Invalidate all call site caches and reset targets to default (cache lookup)
215-
// This ensures metaclass changes are visible without using expensive switchpoint guards
225+
// Invalidate all call site caches and reset targets to default (cache lookup).
226+
// This ensures metaclass changes are visible without using expensive switchpoint guards.
227+
// Synchronize per call site so that checking the target, resetting it, and clearing the cache
228+
// happen atomically with respect to that call site, avoiding races during invalidation.
216229
ALL_CALL_SITES.removeIf(ref -> {
217230
CacheableCallSite cs = ref.get();
218231
if (cs == null) {
219232
return true; // Remove garbage collected references
220233
}
221-
// Reset target to default (fromCache) so next call goes through cache lookup
222-
MethodHandle defaultTarget = cs.getDefaultTarget();
223-
if (defaultTarget != null && cs.getTarget() != defaultTarget) {
224-
cs.setTarget(defaultTarget);
234+
synchronized (cs) {
235+
// Reset target to default (fromCache) so next call goes through cache lookup
236+
MethodHandle defaultTarget = cs.getDefaultTarget();
237+
if (defaultTarget != null && cs.getTarget() != defaultTarget) {
238+
cs.setTarget(defaultTarget);
239+
}
240+
// Clear the cache so stale method handles are discarded
241+
cs.clearCache();
225242
}
226-
// Clear the cache so stale method handles are discarded
227-
cs.clearCache();
228243
return false;
229244
});
230245
}

src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,17 @@ public abstract class Selector {
126126
private static final CallType[] CALL_TYPE_VALUES = CallType.values();
127127

128128
/**
129-
* Whether to use global SwitchPoint guard for metaclass changes.
130-
* Default is false for better performance in frameworks with frequent metaclass changes.
131-
* Set groovy.indy.switchpoint.guard=true to enable strict metaclass change detection.
129+
* Whether to use a global SwitchPoint guard for metaclass changes.
130+
* <p>
131+
* <strong>Default is {@code false}</strong> to avoid the performance cost of globally
132+
* invalidating all indy call sites whenever <em>any</em> metaclass changes.
133+
* Enabling this will significantly degrade performance in frameworks or applications
134+
* with frequent metaclass changes (for example, Grails), because every such change
135+
* forces all guarded call sites to be re-linked.
136+
* <p>
137+
* Set {@code groovy.indy.switchpoint.guard=true} <strong>only</strong> for specific
138+
* debugging or backward-compatibility scenarios where strict metaclass change
139+
* detection is required, and not for general production use.
132140
*/
133141
private static final boolean INDY_SWITCHPOINT_GUARD = SystemUtil.getBooleanSafe("groovy.indy.switchpoint.guard");
134142

@@ -948,14 +956,7 @@ public void setGuards(Object receiver) {
948956
}
949957
}
950958

951-
// Skip the global switchpoint guard by default.
952-
// The switchpoint causes ALL call sites to fail when ANY metaclass changes.
953-
// In Grails and similar frameworks with frequent metaclass changes, this causes
954-
// massive guard failures and performance degradation.
955-
// The other guards (metaclass identity, class receiver, category) should be
956-
// sufficient, combined with cache invalidation on metaclass changes.
957-
//
958-
// If you need strict metaclass change detection, set groovy.indy.switchpoint.guard=true
959+
// Apply global switchpoint guard if enabled (disabled by default for performance)
959960
if (INDY_SWITCHPOINT_GUARD) {
960961
handle = switchPoint.guardWithTest(handle, fallback);
961962
if (LOG_ENABLED) LOG.info("added switch point guard");

0 commit comments

Comments
 (0)