GROOVY-11966: AnnotationNode.isTargetAllowed introduces concurrent wr…#2492
Merged
paulk-asert merged 1 commit intoapache:masterfrom May 1, 2026
Merged
GROOVY-11966: AnnotationNode.isTargetAllowed introduces concurrent wr…#2492paulk-asert merged 1 commit intoapache:masterfrom
paulk-asert merged 1 commit intoapache:masterfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2492 +/- ##
==================================================
- Coverage 67.1394% 67.1372% -0.0022%
+ Complexity 31627 31625 -2
==================================================
Files 1451 1451
Lines 122560 122561 +1
Branches 22007 22007
==================================================
- Hits 82286 82284 -2
- Misses 33194 33195 +1
- Partials 7080 7082 +2
🚀 New features to boost your workflow:
|
e8bbc97 to
ac46a62
Compare
…ite to shared ListHashMap
ac46a62 to
7a7a5b7
Compare
jamesfredley
pushed a commit
to apache/grails-core
that referenced
this pull request
May 2, 2026
Re-audited every Groovy 6 workaround in this canary against the latest Apache Groovy master. Three fixes have merged and are present in the 6.0.0-SNAPSHOT artifact (latest publication: 2026-05-02 11:47:43 UTC, build #546), so the corresponding workarounds can be removed. GROOVY-11968 (apache/groovy#2495), merged 2026-05-01 03:40 UTC, SHA 84f2f37c4f93d6ea44ad8bc76570704c84499c6b - grails-geb/.../ContainerSupport.groovy: revert @CompileDynamic to @CompileStatic now that the trait-static-field VerifyError under indy=false no longer triggers. GROOVY-11967 (apache/groovy#2493), merged 2026-05-01 09:37 UTC, SHA 406feaf5082f1741c318f924b520c4c27bfa0754 - DefaultConstraintFactory.groovy: collapse the two explicit constructors back to a single constructor with a default-valued List parameter; the @CompileStatic VerifyError on the synthesised bridge constructor no longer reproduces. - MappingContextAwareConstraintFactory.groovy: same collapse. GROOVY-11966 (apache/groovy#2492), merged 2026-05-01 18:58 UTC, SHA 8dde1c84134ef6fdeecf26b5cbb5183d5aab4dac - GroovyPageCompiler.groovy: drop the parallelism guard and the grails.gsp.compiler.parallelism system property; restore the original Executors.newFixedThreadPool(availableProcessors() * 2) sizing now that AnnotationNode.isTargetAllowed -> ListHashMap is thread-safe again. - AbstractGroovyTemplateCompiler.groovy: same restoration; drop the grails.views.compiler.parallelism system property. Verified locally on Java 21 / Groovy 6.0.0-SNAPSHOT build #546: ./gradlew :grails-datamapping-validation:compileGroovy ./gradlew :grails-datamapping-core:compileGroovy ./gradlew :grails-gsp-core:compileGroovy ./gradlew :grails-views-core:compileGroovy ./gradlew :grails-geb:compileTestFixturesGroovy -> all BUILD SUCCESSFUL The remaining workarounds (TraitReceiverTransformer static-method override loss, MetaClassImpl genericGetMethod hijack on GORM entities, @CompileStatic named-argument render(Map) silent no-op, smart-cast in 'if (cond && !(x instanceof Y))', VariableScopeVisitor NPE, and ConfigObject [] mutation) have no upstream fix yet and stay in place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…ite to shared ListHashMap
https://issues.apache.org/jira/browse/GROOVY-11966
Background
NodeMetaDataHandlerstores per-ASTNodemetadata in aListHashMap— a small-N optimised map that promotes its internal storage from arrays to aHashMaponce it grows past three entries. This map is not thread-safe, and the array→HashMap promotion insideListHashMap.putis the specific operation that races under concurrent access.Built-in annotation
ClassNodes such as@CompileStatic,@Inject, and@Targetare cached process-wide viaClassHelper.makeCached.AnnotationNode.isTargetAllowedthen memoises an allowed-targets bitmask on each cached node viaclassNode.redirect().getNodeMetaData(Target.class, factory). That call looks like a read but is actually a write into the sharedListHashMap.Any toolchain that compiles Groovy sources in parallel within a single JVM hits this:
Executors.newFixedThreadPool(availableProcessors() * 2)and parallel compilation of pages annotated with@CompileStatic,@Inject, etc. reliably producedArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 ... at org.codehaus.groovy.util.ListHashMap.toMap. Grails worked around it by defaulting GSP compiler parallelism to 1 on Groovy 6 (configurable via-Dgrails.gsp.compiler.parallelism=N) pending an upstream fix.groovy-astare affected similarly.The new reproducer under
subprojects/stressexercises this directly on a shared cachedClassNodefrom 16 threads.Fix
Switch the default
newMetaDataMap()to returnCollections.synchronizedMap(new ListHashMap()). Per-entry operations on the wrapper (get,put,remove,computeIfAbsent) become atomic on a private mutex inside the wrapper, eliminating the race on the array→HashMap promotion. Lazy first-creation of the map field uses a double-checked-locking idiom over avolatile metaDataMapfield onASTNodeandCompileUnit. Iteration over the result ofgetMetaDataMap()is documented in Javadoc as a caller responsibility —Collections#synchronizedMaprequiressynchronized (map) { … }around iteration.ConcurrentHashMapwas the main alternative considered.Trade-off comparison
Collections.synchronizedMap(new ListHashMap())(this PR)ConcurrentHashMapListHashMap)computeIfAbsentatomicityCollections#synchronizedMapoverrides since JDK 8u)getNodeMetaData()no-arg resultunmodifiableMapover an unprotectedListHashMap(unsafe under contention)Map.copyOfsnapshot taken under the wrapper's mutexMap.copyOfsnapshot (lock-free traversal)getMetaDataMap()synchronized (map) { … }per theCollections#synchronizedMapcontractConcurrentModificationExceptionArrayIndexOutOfBoundsException/NullPointerExceptionfrom parallel access toListHashMap(GROOVY-11966)getMetaDataMap()requires caller-side synchronisationExtensibility hook
NodeMetaDataHandler.newMetaDataMap()(added in 5.0.0) remains the extension point for downstream consumers. AST-aware IDE/editor tooling that reuses AST nodes across long-lived sessions has historically overridden it to inject a thread-safe map. With this change, the default is already thread-safe so such overrides are no longer required for correctness; the hook now serves as a performance / customisation knob — for example, downstream consumers may still choose to override withConcurrentHashMapfor workloads that favour lock-free iteration, or with a differently-tunedListHashMapfor memory-constrained scenarios.