I had an issue with Groovy and GraalVM native-image that occurs rarely but consistently. It is especially problematic because it happens only at runtime without any warning from the native-image builder.
I cannot claim I understand all underlying mechanics precisely, but I will try to explain it according to my understanding. Since I spent some time fighting with this thing, I thought it might be helpful for others to know about it and maybe even mention the issue in Groovy documentation somewhere. I did not find any mention of a similar problem online. So, back to the problem. In the native image built with GraalVM, when executing some methods repeatedly for a huge number of times (more than 10000 to be exact), Groovy can cause the following exception: com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, MethodHandle) is reachable The exception is caused by org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:315) method. Here is an example of a full stack trace: 11:41:22.349 [main] INFO o.k.t.g.s.r.r.GradleSourceRepackager - Repackaging Gradle sources: /Users/dmurat/work/dev/croz/klokwrk/klokwrk-project/modules/other/tool/klokwrk-tool-gradle-source-repack/gradle-8.5-all.zip ===> /Users/dmurat/.gradle/caches/8.5/generated-gradle-jars/gradle-api-8.5-sources.jar Exception in thread "main" com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, MethodHandle) is reachable at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:121) at java.base@21.0.1/java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(MethodHandleNatives.java) at java.base@21.0.1/java.lang.invoke.CallSite.setTargetNormal(CallSite.java:291) at java.base@21.0.1/java.lang.invoke.MutableCallSite.setTarget(MutableCallSite.java:155) at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:315) at java.base@21.0.1/java.lang.invoke.LambdaForm$DMH/s7887ae77.invokeStaticInit(LambdaForm$DMH) at org.klokwrk.tool.gradle.source.repack.repackager.GradleSourceRepackagerInfo.getGradleDistributionSrcDirPath(GradleSourceRepackagerInfo.groovy:61) at org.klokwrk.tool.gradle.source.repack.repackager.GradleSourceRepackager$_calculateCountOfTargetZipEntries_closure1$_closure4.doCall(GradleSourceRepackager.groovy:62) at java.base@21.0.1/java.lang.reflect.Method.invoke(Method.java:580) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:343) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:328) at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:279) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1008) at groovy.lang.Closure.call(Closure.java:433) at org.codehaus.groovy.runtime.ConvertedClosure.invokeCustom(ConvertedClosure.java:52) at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:113) at jdk.proxy1/jdk.proxy1.$Proxy50.test(Unknown Source) at java.base@21.0.1/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178) at java.base@21.0.1/java.util.zip.ZipFile$EntrySpliterator.tryAdvance(ZipFile.java:573) at java.base@21.0.1/java.util.Spliterator.forEachRemaining(Spliterator.java:332) at java.base@21.0.1/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) at java.base@21.0.1/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base@21.0.1/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) at java.base@21.0.1/java.util.stream.ReduceOps$5.evaluateSequential(ReduceOps.java:258) at java.base@21.0.1/java.util.stream.ReduceOps$5.evaluateSequential(ReduceOps.java:248) at java.base@21.0.1/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base@21.0.1/java.util.stream.ReferencePipeline.count(ReferencePipeline.java:709) at org.klokwrk.tool.gradle.source.repack.repackager.GradleSourceRepackager$_calculateCountOfTargetZipEntries_closure1.doCall(GradleSourceRepackager.groovy:61) at java.base@21.0.1/java.lang.reflect.Method.invoke(Method.java:580) at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:343) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:328) at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:279) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1008) at groovy.lang.Closure.call(Closure.java:433) at groovy.lang.Closure.call(Closure.java:422) at org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable(IOGroovyMethods.java:1616) at org.klokwrk.tool.gradle.source.repack.repackager.GradleSourceRepackager.calculateCountOfTargetZipEntries(GradleSourceRepackager.groovy:59) at org.klokwrk.tool.gradle.source.repack.repackager.GradleSourceRepackager.repackGradleSource(GradleSourceRepackager.groovy:53) at org.klokwrk.tool.gradle.source.repack.GradleSourceRepackCommand.run(GradleSourceRepackCommand.groovy:130) at picocli.CommandLine.executeUserObject(CommandLine.java:2026) at picocli.CommandLine.access$1500(CommandLine.java:148) at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461) at picocli.CommandLine$RunLast.handle(CommandLine.java:2453) at picocli.CommandLine$RunLast.handle(CommandLine.java:2415) at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273) at picocli.CommandLine$RunLast.execute(CommandLine.java:2417) at picocli.CommandLine.execute(CommandLine.java:2170) at io.micronaut.configuration.picocli.PicocliRunner.run(PicocliRunner.java:137) at io.micronaut.configuration.picocli.PicocliRunner.run(PicocliRunner.java:114) at org.klokwrk.tool.gradle.source.repack.GradleSourceRepackCommand.main(GradleSourceRepackCommand.groovy:64) When looking at the source, the corresponding "if" branch is triggered, besides other things, because "INDY_OPTIMIZE_THRESHOLD" is exceeded. After looking at the definition of the constant, it is visible that it is read from the "groovy.indy.optimize.threshold" system property and has a default of 10_000. I tried to change the value to 100_000 by passing the property to the native executable, but that does not work, unfortunately. The property must be set during native image building to become effective. I've used the following options with native-builder: "-J-Dgroovy.indy.optimize.threshold=100000", "-J-Dgroovy.indy.fallback.threshold=100000" As can be seen, besides "groovy.indy.optimize.threshold," there is also a related "groovy.indy.fallback.threshold" system property set. After those changes, the created native image no longer throws the exception mentioned above. I hope this description will be helpful to anyone encountering similar issues. Best regards, Damir Murat