[
https://issues.apache.org/jira/browse/GROOVY-12307?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King resolved GROOVY-12307.
--------------------------------
Fix Version/s: 6.0.0-beta-4
Resolution: Fixed
> Indy: MetaClass change during call-site linking can be lost
> -----------------------------------------------------------
>
> Key: GROOVY-12307
> URL: https://issues.apache.org/jira/browse/GROOVY-12307
> Project: Groovy
> Issue Type: Bug
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
> Fix For: 6.0.0-beta-4
>
>
> The GROOVY-12281 stress test intermittently fails with a per-thread
> monotonicity violation:
> {noformat}
> ClassInfoSoftModeStressTest >
> softModeSurvivesRealGcClearingUnderConcurrency() FAILED
> stress probe failed: dispatches=3721845 generations=114
> javaInfosCollected=33/36
> ERROR: mutated-1: IllegalStateException: mutated-1 observed gen 13 after
> already seeing 14
> {noformat}
> i.e. a dispatcher thread observed an older ExpandoMetaClass generation
> *after* already observing a newer one. The stress environment (soft
> ClassValue mode plus GC churn) only raises the relink frequency; the
> underlying race is independent of soft mode.
> h3. Root cause
> Indy call-site linking reads MOP state and installs its guard in the wrong
> order:
> # the fallback resolves the MetaClass and selects the target method;
> # only afterwards ({{Selector.setGuards}} ->
> {{IndyInterface.applyMopSwitchPoints}}) does it fetch the *current*
> class-domain SwitchPoint to guard that target.
> A MetaClass change landing between (1) and (2) retires the old SwitchPoint
> and rotates in a fresh one — which is exactly the token the racing relink
> then fetches. The stale selection is published under a live guard, clobbering
> any newer target already installed at the shared call site. The site serves
> the pre-change selection until an unrelated future invalidation; if the race
> lands on the last change, the staleness is permanent (a MetaClass mutation is
> silently lost).
> The same time-of-check/time-of-use hole exists in the cold reflective tier
> ({{ColdReflectiveMethodHandleWrapper.tryBuild}} captures {{classValidity}}
> after selection) and in the GEP-15 compound-assign resolver
> ({{IndyCompoundAssign.resolve}} fetches the SwitchPoint after
> {{respondsTo}}/selection).
> h3. Fix
> Capture the class-domain SwitchPoint *before* selection first reads MOP
> state, and install the guard with that captured token: a change racing the
> selection then invalidates the token, so the guard routes to the fallback and
> re-links instead of publishing the stale selection.
> Two complications:
> * *Born-dead tokens:* lazy MetaClass creation during resolution bumps the
> class generation, invalidating a token captured before the first-ever read
> (an always-fallback guard then recurses to StackOverflowError). The capture
> therefore retries: capture token, resolve the MetaClass, re-capture if the
> token died during resolution — the next pass finds the cached instance and
> converges.
> * *Pathological re-creation:* some resolutions re-create the MetaClass on
> every lookup (e.g. ClosureMetaClass under
> {{ExpandoMetaClass.enableGlobally()}}, which {{isValidWeakMetaClass}} always
> rejects), so no pass can end with a live token and an unbounded retry
> livelocks. The retry is bounded (3 passes); on exhaustion the selection
> degrades to the existing uncacheable path — no guards, fresh selection per
> call — which is correct and cannot livelock or recurse.
> A deterministic regression test in {{IndyScopedSwitchPointTest}} encodes the
> contract (a token captured before a mid-selection mutation must route to the
> fallback); the stress test remains the probabilistic end-to-end check.
> The classic (non-indy) {{CallSiteArray}} path has not been audited for an
> analogous window and may warrant a follow-up.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)