Skip to content

Comments

Add AOT/Native sample w/ integration test#120

Merged
onobc merged 1 commit intospring-projects:mainfrom
therepanic:add-aot-integration-test
Mar 10, 2025
Merged

Add AOT/Native sample w/ integration test#120
onobc merged 1 commit intospring-projects:mainfrom
therepanic:add-aot-integration-test

Conversation

@therepanic
Copy link
Contributor

Since I don't have much expertise in this area, I spent some time, but I still managed to combine Spring AOT + native image. I did this by adding the org.springframework.boot.aot plugin for AOT, and to make it all native, I added BP_NATIVE_IMAGE=true to the environment.

And everything is as described in the issue, we can't get the project to build. When trying to build, we will see the following log:

    [creator]     Error: Classes that should be initialized at run time got initialized during image building:
    [creator]      ch.qos.logback.classic.Logger was unintentionally initialized at build time. To see why ch.qos.logback.classic.Logger got initialized use --trace-class-initialization=ch.qos.logback.classic.Logger
    [creator]     ch.qos.logback.core.util.StatusPrinter was unintentionally initialized at build time. To see why ch.qos.logback.core.util.StatusPrinter got initialized use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter
    [creator]     ch.qos.logback.core.util.StatusPrinter2 was unintentionally initialized at build time. To see why ch.qos.logback.core.util.StatusPrinter2 got initialized use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter2
    [creator]     org.slf4j.helpers.Reporter was unintentionally initialized at build time. To see why org.slf4j.helpers.Reporter got initialized use --trace-class-initialization=org.slf4j.helpers.Reporter
    [creator]     ch.qos.logback.core.status.InfoStatus was unintentionally initialized at build time. To see why ch.qos.logback.core.status.InfoStatus got initialized use --trace-class-initialization=ch.qos.logback.core.status.InfoStatus
    [creator]     ch.qos.logback.core.util.Loader was unintentionally initialized at build time. To see why ch.qos.logback.core.util.Loader got initialized use --trace-class-initialization=ch.qos.logback.core.util.Loader
    [creator]     ch.qos.logback.core.status.StatusBase was unintentionally initialized at build time. To see why ch.qos.logback.core.status.StatusBase got initialized use --trace-class-initialization=ch.qos.logback.core.status.StatusBase
    [creator]     org.slf4j.LoggerFactory was unintentionally initialized at build time. To see why org.slf4j.LoggerFactory got initialized use --trace-class-initialization=org.slf4j.LoggerFactory
    [creator]     ch.qos.logback.classic.Level was unintentionally initialized at build time. To see why ch.qos.logback.classic.Level got initialized use --trace-class-initialization=ch.qos.logback.classic.Level
    [creator]     To see how the classes got initialized, use --trace-class-initialization=ch.qos.logback.classic.Logger,ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.core.util.StatusPrinter2,org.slf4j.helpers.Reporter,ch.qos.logback.core.status.InfoStatus,ch.qos.logback.core.util.Loader,ch.qos.logback.core.status.StatusBase,org.slf4j.LoggerFactory,ch.qos.logback.classic.Level
    [creator]     Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
    [creator]     --------------------------------------------------------------------------------
    [creator]         7.1s (14.6% of total time) in 87 GCs | Peak RSS: 1.96GB | CPU load: 3.65
    [creator]     ================================================================================
    [creator]     Finished generating 'org.springframework.grpc.sample.GrpcServerApplication' in 48.0s.
    [creator]     unable to invoke layer creator
    [creator]     unable to contribute native-image layer
    [creator]     error running build
    [creator]     exit status 1
    [creator]     ERROR: failed to build: exit status 1

> Task :bootBuildImage FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':bootBuildImage'.
> Builder lifecycle 'creator' failed with status code 51

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 1m 26s
13 actionable tasks: 13 executed

Is it just me, or is it just that problem related to missing hints?

fix: #116

@therepanic
Copy link
Contributor Author

Also, if this is what we need, should we now, as I did, write that the build will not succeed until the missing hints problem is fixed?

@therepanic therepanic force-pushed the add-aot-integration-test branch from 2f54286 to 10b8b09 Compare February 28, 2025 21:56
Copy link
Contributor

@onobc onobc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great first pass at this @panic08 !

A have left some specific comments inline. Other than that, here are some general thoughts:

So we can build native images using Buildpacks or w/ GraalVM native build tools.

I think we should support and talk about both.

Buildpacks

With buildpacks we build w/ one of these:

mvn -Pnative spring-boot:build-image

OR

gradle bootBuildImage

Then run in Docker like:

docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT

Native Build Tools

Requirement here is that we have GraalVM installed on the machine. I use skdman locally and do something like:

$ sdk install java 22.3.r17-nik
$ sdk use java 22.3.r17-nik

I am not sure how we go about handling this in an automated fashion though. I believe we can leverage org.junit.jupiter.api.condition.EnabledInNativeImage to guard the tests in this case.

Note

Let's not worry about the scaffolding or executing it w/ native tools but let's make sure that the steps we end up w/ in the README/help are valid and we can do this locally.

After that the steps to build the native executable are..

mvn -Pnative native:compile

OR

gradle nativeCompile

And to run

target/grpc-server-aot

OR

build/native/nativeCompile/grpc-server-aot

Wdyt?

@onobc onobc changed the title Add AOT integration test Add AOT/Native sample w/ integration test Mar 2, 2025
@therepanic therepanic force-pushed the add-aot-integration-test branch 3 times, most recently from 0fdbeda to 75cef70 Compare March 4, 2025 21:10
@therepanic
Copy link
Contributor Author

Thank you so much for the review @onobc

I've fixed the unnecessary things you pointed out and brought the module as close as possible to what is generated in start.spring.io

Wdyt?

I actually added the org.springframework.boot.aot plugin initially for a reason. It was with it that my build crashed, and I originally thought that it should crash, and that with org.graalvm.buildtools.native Spring AOT is not activated.

If we run a native build with the org.graalvm.buildtools.native plugin and use Native Build Tools or Buildpacks, we will somehow successfully build the project and be able to run it without crashing.

However, we will see some warnings in the logs. Here are the warnings that appear if I start building natively with ./gradlew nativeCompile:

Warning: Could not resolve class com.github.luben.zstd.ZstdCompressCtx for reflection configuration. Reason: java.lang.ClassNotFoundException: com.github.luben.zstd.ZstdCompressCtx.
Warning: Could not resolve class io.netty.handler.ssl.OpenSslClientSessionCache for reflection configuration. Reason: java.lang.NoClassDefFoundError: io/netty/internal/tcnative/SSLSessionCache.
Warning: Could not resolve class io.netty.handler.ssl.ReferenceCountedOpenSslClientContext$ExtendedTrustManagerVerifyCallback for reflection configuration. Reason: java.lang.NoClassDefFoundError: io/netty/internal/tcnative/CertificateVerifier.
Warning: Could not resolve class io.netty.handler.ssl.ReferenceCountedOpenSslServerContext$ExtendedTrustManagerVerifyCallback for reflection configuration. Reason: java.lang.NoClassDefFoundError: io/netty/internal/tcnative/CertificateVerifier.
Warning: Could not resolve class io.netty.handler.ssl.ReferenceCountedOpenSslServerContext$OpenSslServerCertificateCallback for reflection configuration. Reason: java.lang.NoClassDefFoundError: io/netty/internal/tcnative/CertificateCallback.
Warning: Could not resolve class org.conscrypt.ConscryptEngine for reflection configuration. Reason: java.lang.ClassNotFoundException: org.conscrypt.ConscryptEngine.
Warning: Could not resolve class org.conscrypt.ConscryptEngine for reflection configuration. Reason: java.lang.ClassNotFoundException: org.conscrypt.ConscryptEngine.
Warning: Could not resolve class org.conscrypt.ConscryptEngine for reflection configuration. Reason: java.lang.ClassNotFoundException: org.conscrypt.ConscryptEngine.
Warning: Could not resolve class io.netty.resolver.dns.macos.DnsResolver for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.resolver.dns.macos.DnsResolver.
Warning: Could not resolve class io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider.
Warning: Could not resolve class io.netty.channel.kqueue.BsdSocket for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.channel.kqueue.BsdSocket.
Warning: Could not resolve class io.netty.channel.kqueue.KQueueEventArray for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.channel.kqueue.KQueueEventArray.
Warning: Could not resolve class io.netty.channel.kqueue.KQueueStaticallyReferencedJniMethods for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.channel.kqueue.KQueueStaticallyReferencedJniMethods.
Warning: Could not resolve class io.netty.channel.kqueue.Native for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.channel.kqueue.Native.
Warning: Method ch.qos.logback.core.encoder.LayoutWrappingEncoder.setParent(Appender) not found.
Warning: Could not resolve class com.aayushatharva.brotli4j.Brotli4jLoader for reflection configuration. Reason: java.lang.ClassNotFoundException: com.aayushatharva.brotli4j.Brotli4jLoader.
Warning: Could not resolve class com.github.luben.zstd.Zstd for reflection configuration. Reason: java.lang.ClassNotFoundException: com.github.luben.zstd.Zstd.
Warning: Could not resolve class com.ning.compress.lzf.impl.UnsafeChunkDecoder for reflection configuration. Reason: java.lang.ClassNotFoundException: com.ning.compress.lzf.impl.UnsafeChunkDecoder.

Warning: Could not resolve class io.netty.channel.epoll.EpollSocketChannel for reflection configuration. Reason: java.lang.ClassNotFoundException: io.netty.channel.epoll.EpollSocketChannel.
Warning: Method java.nio.DirectByteBuffer.<init>(long, long) not found.
Warning: Method java.nio.DirectByteBuffer.<init>(long, long) not found.
Warning: Could not resolve class org.apache.commons.logging.impl.Log4JLogger for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.commons.logging.impl.Log4JLogger.
Warning: Could not resolve class org.apache.commons.logging.impl.LogFactoryImpl for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.commons.logging.impl.LogFactoryImpl.
Warning: Could not resolve class org.apache.commons.logging.impl.WeakHashtable for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.commons.logging.impl.WeakHashtable.
Warning: Could not resolve class org.apache.log4j.Level for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.log4j.Level.
Warning: Could not resolve class org.apache.log4j.Priority for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.log4j.Priority.
Warning: Could not resolve class org.conscrypt.Conscrypt for reflection configuration. Reason: java.lang.ClassNotFoundException: org.conscrypt.Conscrypt.
Warning: Could not resolve class org.conscrypt.OpenSSLContextImpl$TLSv13 for reflection configuration. Reason: java.lang.ClassNotFoundException: org.conscrypt.OpenSSLContextImpl$TLSv13.
Warning: Could not resolve class org.hamcrest.number.OrderingComparison for reflection configuration. Reason: java.lang.ClassNotFoundException: org.hamcrest.number.OrderingComparison.
Warning: Method ch.qos.logback.core.FileAppender.valueOf(String) not found.
Warning: Method ch.qos.logback.core.Layout.valueOf(String) not found.
Warning: Method ch.qos.logback.core.rolling.RollingPolicy.valueOf(String) not found.
Warning: Method ch.qos.logback.core.rolling.TriggeringPolicy.valueOf(String) not found.
Warning: Method ch.qos.logback.core.spi.ContextAware.valueOf(String) not found.
Warning: Could not resolve class jakarta.inject.Inject for reflection configuration. Reason: java.lang.ClassNotFoundException: jakarta.inject.Inject.
Warning: Could not resolve class jakarta.inject.Provider for reflection configuration. Reason: java.lang.ClassNotFoundException: jakarta.inject.Provider.
Warning: Could not resolve class jakarta.inject.Qualifier for reflection configuration. Reason: java.lang.ClassNotFoundException: jakarta.inject.Qualifier.
Warning: Could not resolve class javax.inject.Inject for reflection configuration. Reason: java.lang.ClassNotFoundException: javax.inject.Inject.
Warning: Could not resolve class javax.inject.Qualifier for reflection configuration. Reason: java.lang.ClassNotFoundException: javax.inject.Qualifier.
Warning: Could not resolve class javax.money.MonetaryAmount for reflection configuration. Reason: java.lang.ClassNotFoundException: javax.money.MonetaryAmount.
Warning: Could not resolve class kotlin.Metadata for reflection configuration. Reason: java.lang.ClassNotFoundException: kotlin.Metadata.
Warning: Could not resolve class kotlin.reflect.full.KClasses for reflection configuration. Reason: java.lang.ClassNotFoundException: kotlin.reflect.full.KClasses.
Warning: Could not resolve class org.eclipse.core.runtime.FileLocator for reflection configuration. Reason: java.lang.ClassNotFoundException: org.eclipse.core.runtime.FileLocator.
Warning: Could not resolve class org.reactivestreams.Publisher for reflection configuration. Reason: java.lang.ClassNotFoundException: org.reactivestreams.Publisher.
[1/8] Initializing...
                                                         
...

If we build with Build Packs, everything will also start and run fine, these warnings will still appear during the build phase

@therepanic therepanic requested a review from onobc March 4, 2025 21:17
@dsyer
Copy link
Member

dsyer commented Mar 5, 2025

Do we really need a completely new sample? Wouldn't the existing grpc-server sample be minimal enough, and it already has the native plugin configured in Maven?

@therepanic
Copy link
Contributor Author

Do we really need a completely new sample? Wouldn't the existing grpc-server sample be minimal enough, and it already has the native plugin configured in Maven?

We take the new module as an it and its domain is exactly testing this functionality, as I think the new module will be not bad for this, what do you think?

@dsyer
Copy link
Member

dsyer commented Mar 5, 2025

I don't know what you mean. The old module is just fine IMO (it even works OOTB in Maven - for Gradle I had to add some settings).

@therepanic
Copy link
Contributor Author

I don't know what you mean. The old module is just fine IMO (it even works OOTB in Maven - for Gradle I had to add some settings).

I mean that this kind of integration test in my opinion is better to see in a separate module. I understand @onobc is of the same opinion

@onobc
Copy link
Contributor

onobc commented Mar 5, 2025

I don't know what you mean. The old module is just fine IMO (it even works OOTB in Maven - for Gradle I had to add some settings).

My reasoning on creating a single module is:

  1. separation of concerns - have one sample that does AOT/native and does it well and gives excellent docs on how to do so in Gradle vs. Maven
  2. reduce maintenance toil - by not having AOT/native aspects spread across each sample it will likely reduce the maintenance footprint for the samples

I would propose (if we go forward w/ single AOT/native sample) that we remove the AOT/native mentions/configs from the other samples.

That being said, my goal is more to have AOT/native in a single sample; whether it be in the existing grpc-server sample or a new one.

@dsyer
Copy link
Member

dsyer commented Mar 5, 2025

I don't really see how this separate module is better than the one we already have (the grpc-server). IMO it would be more interesting to concentrate on some github action declarations that we can use to verify that native images are working.

@onobc
Copy link
Contributor

onobc commented Mar 5, 2025

I don't really see how this separate module is better than the one we already have (the grpc-server). IMO it would be more interesting to concentrate on some github action declarations that we can use to verify that native images are working.

I updated my previous comment w/ the goal of just having the native/AOT in a single place (rather than its own module). Is that a good compromise?

@onobc
Copy link
Contributor

onobc commented Mar 5, 2025

We discussed this during our team meeting today and have agreed to put this AOT/native into the existing grpc-server sample. Sorry for the back and forth @panic08 .

Do you mind targeting the relevant changes to the grpc-server sample instead?

@therepanic
Copy link
Contributor Author

We discussed this during our team meeting today and have agreed to put this AOT/native into the existing grpc-server sample. Sorry for the back and forth @panic08 .

Do you mind targeting the relevant changes to the grpc-server sample instead?

I guess we decided not to make a separate module after all, but to have just grpc-server. Then I guess as it sounds, of the changes that will be moved to grpc-server, it's only HELP-gradle.md, HELP-maven.md.

Anyway, after I move to grpc-server, I guess the build results will be the same as here #120 (comment). So the build is still successful? Now it is worth to add I guess workflow as dsyer said, which confirms that the build is successful?

@dsyer
Copy link
Member

dsyer commented Mar 6, 2025

Did you test the native image build (maven and gradle)? It didn't work for me in Gradle.

@therepanic
Copy link
Contributor Author

Did you test the native image build (maven and gradle)? It didn't work for me in Gradle.

In my current module native build via gradle works, I've discounted the log above. I'll try to see what's wrong with the grpc-server build

@onobc
Copy link
Contributor

onobc commented Mar 6, 2025

Now it is worth to add I guess workflow as dsyer said, which confirms that the build is successful?

We want to hold off on the workflows for a bit as there are some that we may be able leverage from other Spring projects.

@therepanic therepanic force-pushed the add-aot-integration-test branch from 75cef70 to d0d2140 Compare March 6, 2025 21:00
@therepanic
Copy link
Contributor Author

Did you test the native image build (maven and gradle)? It didn't work for me in Gradle.

In my current module native build via gradle works, I've discounted the log above. I'll try to see what's wrong with the grpc-server build

I have native image builds working successfully with both Build Packs and Native Build Tools (gradle and maven respectively). I run all this on Ubuntu 22.04, also using sdkman with dependency: 17.0.9-graalce

@therepanic therepanic force-pushed the add-aot-integration-test branch from d0d2140 to 4310365 Compare March 6, 2025 21:02
Copy link
Contributor

@onobc onobc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job @panic08 !
I have a minor ask on the README, but other than that this looks great.

@therepanic
Copy link
Contributor Author

By the way, what do you think about adding HELP-.md to each sample?

Since we gave up a separate module with aot + native image, I think it would be worth supporting native images for each sample then. And right now we have such support (added org.graalvm.buildtools.native plugin to each sample and everything works well).

Then I think we should add HELP-.md to each sample?

@onobc
Copy link
Contributor

onobc commented Mar 7, 2025

By the way, what do you think about adding HELP-.md to each sample?

Since we gave up a separate module with aot + native image, I think it would be worth supporting native images for each sample then. And right now we have such support (added org.graalvm.buildtools.native plugin to each sample and everything works well).

Then I think we should add HELP-.md to each sample?

I'm actually in favor of removing native from all other samples as it can become a maintenance headache. I am in favor of doing it in one place and doing it well. Let's just focus on grpc-server for now and then we can decide what other samples it makes sense to add it to.

@therepanic therepanic force-pushed the add-aot-integration-test branch from 4310365 to 726f8f4 Compare March 9, 2025 21:21
Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
@therepanic therepanic force-pushed the add-aot-integration-test branch from 726f8f4 to 31f2bab Compare March 9, 2025 21:23
@therepanic therepanic requested a review from onobc March 9, 2025 21:23
Copy link
Contributor

@onobc onobc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good @panic08 ! Thanks for another great contribution.

@onobc onobc merged commit ec9eb13 into spring-projects:main Mar 10, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AOT integration test

3 participants