Skip to content

Commit 2a5e983

Browse files
author
James Fredley
committed
drop Groovy 6 workarounds whose upstream fixes have merged
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.
1 parent 367dad7 commit 2a5e983

5 files changed

Lines changed: 7 additions & 146 deletions

File tree

grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/constraints/MappingContextAwareConstraintFactory.groovy

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@ class MappingContextAwareConstraintFactory extends DefaultConstraintFactory {
3535

3636
final MappingContext mappingContext
3737

38-
MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext) {
39-
this(constraintClass, messageSource, mappingContext, [Object] as List<Class>)
40-
}
41-
42-
MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext, List<Class> targetTypes) {
38+
MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext, List<Class> targetTypes = [Object]) {
4339
super(constraintClass, messageSource, targetTypes)
4440
this.mappingContext = mappingContext
4541
}

grails-datamapping-validation/src/main/groovy/org/grails/datastore/gorm/validation/constraints/factory/DefaultConstraintFactory.groovy

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,7 @@ class DefaultConstraintFactory implements ConstraintFactory {
4747

4848
protected final Constructor constraintConstructor
4949

50-
DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource) {
51-
this(constraintClass, messageSource, [Object] as List<Class>)
52-
}
53-
54-
DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, List<Class> targetTypes) {
50+
DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, List<Class> targetTypes = [Object]) {
5551
this.type = constraintClass
5652
this.name = Introspector.decapitalize(constraintClass.simpleName) - 'Constraint'
5753
this.messageSource = messageSource

grails-geb/src/testFixtures/groovy/grails/plugin/geb/support/ContainerSupport.groovy

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,7 @@ import grails.plugin.geb.ContainerGebSpec
3434
* @author Mattias Reichel
3535
* @since 4.2
3636
*/
37-
// GROOVY-11907 follow-up / indy=false bytecode bug: @CompileStatic on a trait
38-
// with static fields generates invalid bytecode for the static setter helpers
39-
// when the downstream consumer is compiled with grailsIndy=false. The
40-
// Trait$Helper methods come out with mismatched local slots (e.g. dload_3 on
41-
// a 2-local frame) and trip a JVM VerifyError ("get long/double overflows
42-
// locals") at ContainerGebSpec class init, cascading into NoClassDefFoundError
43-
// on every spec that extends ContainerGebSpec.
44-
//
45-
// Fixed for Groovy 5.0.6-SNAPSHOT (commit 74da8078b5 on grails8-groovy5-sb4
46-
// restored @CompileStatic for Groovy 5). Tracked upstream as GROOVY-11968
47-
// (apache/groovy PR #2495 by @paulk-asert):
48-
// https://issues.apache.org/jira/browse/GROOVY-11968
49-
// https://github.com/apache/groovy/pull/2495
50-
//
51-
// Standalone reproducer (`TraitStaticFieldsCheck.groovy` in `quick-checks/`):
52-
// https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/tree/main/quick-checks
53-
//
54-
// Status to re-verify against latest Groovy 6.0.0-SNAPSHOT before each canary
55-
// rebuild: if the fix has propagated to the Groovy 6 line, drop @CompileDynamic
56-
// and switch back to @CompileStatic.
57-
@CompileDynamic
37+
@CompileStatic
5838
@SelfType(ContainerGebSpec)
5939
trait ContainerSupport implements DownloadSupport {
6040

grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageCompiler.groovy

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -109,25 +109,11 @@ class GroovyPageCompiler {
109109
}
110110
compilerConfig.setTargetDirectory(targetDir)
111111
compilerConfig.setSourceEncoding(encoding)
112-
// GSP compilation parallelism is intentionally configurable via the
113-
// grails.gsp.compiler.parallelism system property. The default is
114-
// 1 (serial) under Groovy 6 because Groovy 6.0.0-SNAPSHOT contains
115-
// a thread-safety bug in org.codehaus.groovy.util.ListHashMap that
116-
// surfaces during AnnotationNode.isTargetAllowed -> NodeMetaDataHandler
117-
// .getNodeMetaData -> Map.computeIfAbsent on shared annotation
118-
// metadata (e.g. @Inject, @CompileStatic) when multiple GSPs are
119-
// compiled concurrently. The symptom is "General error during
120-
// instruction selection: Index N out of bounds for length N" with
121-
// an ArrayIndexOutOfBoundsException in ListHashMap.toMap. Falling
122-
// back to a single thread eliminates the race at a small cost in
123-
// wall-clock time. Override with -Dgrails.gsp.compiler.parallelism=N
124-
// (or 0 to use availableProcessors*2) once Groovy 6 fixes this.
125-
int parallelism = computeGspCompilerParallelism()
126-
ExecutorService threadPool = Executors.newFixedThreadPool(parallelism)
112+
ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
127113
CompletionService completionService = new ExecutorCompletionService(threadPool)
128114
List<Future<Map>> futures = []
129115
try {
130-
Integer collationLevel = parallelism
116+
Integer collationLevel = Runtime.getRuntime().availableProcessors() * 2
131117
if (srcFiles.size() < collationLevel) {
132118
collationLevel = 1
133119
}
@@ -188,48 +174,6 @@ class GroovyPageCompiler {
188174
return compileGSPRegistry
189175
}
190176

191-
/**
192-
* Resolves the worker-thread count for parallel GSP compilation.
193-
*
194-
* Honours -Dgrails.gsp.compiler.parallelism=N. A value of 0 (or any
195-
* non-positive number) means "use availableProcessors() * 2" (the
196-
* historical Grails default). When the property is unset we default
197-
* to 1 on Groovy 6 (see the inline comment at the call site for why)
198-
* and to availableProcessors() * 2 on Groovy 5 and earlier.
199-
*/
200-
private static int computeGspCompilerParallelism() {
201-
int cores = Runtime.getRuntime().availableProcessors()
202-
int defaultParallelism = isGroovy6OrLater() ? 1 : cores * 2
203-
204-
String override = System.getProperty('grails.gsp.compiler.parallelism')
205-
if (override == null || override.isEmpty()) {
206-
return defaultParallelism
207-
}
208-
try {
209-
int requested = Integer.parseInt(override.trim())
210-
if (requested <= 0) {
211-
return cores * 2
212-
}
213-
return requested
214-
} catch (NumberFormatException ignore) {
215-
return defaultParallelism
216-
}
217-
}
218-
219-
private static boolean isGroovy6OrLater() {
220-
String version = groovy.lang.GroovySystem.getVersion()
221-
if (version == null || version.isEmpty()) {
222-
return false
223-
}
224-
try {
225-
int dot = version.indexOf('.')
226-
int major = Integer.parseInt(dot >= 0 ? version.substring(0, dot) : version)
227-
return major >= 6
228-
} catch (NumberFormatException ignore) {
229-
return false
230-
}
231-
}
232-
233177
/**
234178
* Compiles an individual GSP file
235179
*

grails-views-core/src/main/groovy/grails/views/AbstractGroovyTemplateCompiler.groovy

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,11 @@ abstract class AbstractGroovyTemplateCompiler {
8282

8383
void compile(List<File> sources) {
8484

85-
// Mirror the GSP-side guard in GroovyPageCompiler: Groovy 6.0.0-SNAPSHOT
86-
// contains a thread-safety bug in org.codehaus.groovy.util.ListHashMap
87-
// reachable through AnnotationNode.isTargetAllowed ->
88-
// NodeMetaDataHandler.getNodeMetaData -> Map.computeIfAbsent on shared
89-
// annotation metadata when multiple template compiles concurrently
90-
// touch the same AST. Surfaces in CI as
91-
// General error during instruction selection: Index N out of bounds
92-
// java.lang.ArrayIndexOutOfBoundsException ... at ListHashMap.toMap
93-
// during :grails-test-examples-*:compileGsonViews. Default to a single
94-
// worker on Groovy 6 to dodge the race; preserve the historical
95-
// availableProcessors() * 2 default on Groovy 5 and earlier. Override
96-
// with -Dgrails.views.compiler.parallelism=N once Groovy 6 fixes this
97-
// (or 0 to use availableProcessors() * 2 explicitly).
98-
int parallelism = computeParallelism()
99-
ExecutorService threadPool = Executors.newFixedThreadPool(parallelism)
85+
ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
10086
CompletionService completionService = new ExecutorCompletionService(threadPool)
10187

10288
try {
103-
Integer collationLevel = parallelism
89+
Integer collationLevel = Runtime.getRuntime().availableProcessors() * 2
10490
if (sources.size() < collationLevel) {
10591
collationLevel = 1
10692
}
@@ -157,47 +143,6 @@ abstract class AbstractGroovyTemplateCompiler {
157143
compile(Arrays.asList(sources))
158144
}
159145

160-
/**
161-
* Resolves the worker-thread count for parallel template compilation.
162-
* Honours -Dgrails.views.compiler.parallelism=N. A non-positive override
163-
* means "use availableProcessors() * 2" (the historical default). When the
164-
* property is unset we default to 1 on Groovy 6 (see the inline comment at
165-
* the call site for the ListHashMap thread-safety reasoning) and to
166-
* availableProcessors() * 2 on Groovy 5 and earlier.
167-
*/
168-
private static int computeParallelism() {
169-
int cores = Runtime.getRuntime().availableProcessors()
170-
int defaultParallelism = isGroovy6OrLater() ? 1 : cores * 2
171-
172-
String override = System.getProperty('grails.views.compiler.parallelism')
173-
if (override == null || override.isEmpty()) {
174-
return defaultParallelism
175-
}
176-
try {
177-
int requested = Integer.parseInt(override.trim())
178-
if (requested <= 0) {
179-
return cores * 2
180-
}
181-
return requested
182-
} catch (NumberFormatException ignore) {
183-
return defaultParallelism
184-
}
185-
}
186-
187-
private static boolean isGroovy6OrLater() {
188-
String version = groovy.lang.GroovySystem.getVersion()
189-
if (version == null || version.isEmpty()) {
190-
return false
191-
}
192-
try {
193-
int dot = version.indexOf('.')
194-
int major = Integer.parseInt(dot >= 0 ? version.substring(0, dot) : version)
195-
return major >= 6
196-
} catch (NumberFormatException ignore) {
197-
return false
198-
}
199-
}
200-
201146
static void run(String[] args, Class<? extends GenericViewConfiguration> configurationClass, Class<? extends AbstractGroovyTemplateCompiler> compilerClass) {
202147
if (args.length != 7) {
203148
System.err.println("Invalid arguments: [${args.join(',')}]")

0 commit comments

Comments
 (0)