Skip to content

Fix NPE caused by uninitialized static variables in interfaces in native images #3256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dev-jonghoonpark
Copy link
Contributor

related issue: #3168

The DEFAULT_SCHEDULER variable in the BaseAdvisor interface was being initialized to null when running a native image.

To address this issue, a separate SchedulerHolder class was created to handle the variable initialization properly and ensure it is initialized as expected.

@dev-jonghoonpark dev-jonghoonpark force-pushed the GH-3168-fix-npe-in-native-image branch from a10815b to 4dd82c3 Compare May 20, 2025 11:36
@markpollack
Copy link
Member

I don't understand what is going on here,currently in BaseAdvisor there is this code

public interface BaseAdvisor extends CallAdvisor, StreamAdvisor {

	Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();

Why would this field be set to null when creating a native image?

@joshlong
Copy link
Member

joshlong commented May 29, 2025

hi - could you tell us against what version of Spring AI you're reporting this? and against what version of GraalVM? Which OS?

ive attempted to reproduce the bug here [1] on macOS using graalvm for java 24 and haven't yet seen the issue...

I wonder if having some sort of proxy / AOP logic is causing that reference to be null? Are you defining the ChatMemoryAdvisor as a bean? Is it proxied at some point? What happens if you instantiate it inline, as i have?

Alternatively: is it possible you don't have the Reactor libraries on the class path? I don't know..

  1. https://github.com/joshlong-attic/2025-05-29-spring-ai-graalvm-advisor-npe-3256

@joshlong
Copy link
Member

hi - could you tell us against what version of Spring AI you're reporting this? and against what version of GraalVM? Which OS?

ive attempted to reproduce the bug here [1] on macOS using graalvm for java 24 and haven't yet seen the issue...

I wonder if having some sort of proxy / AOP logic is causing that reference to be null? Are you defining the ChatMemoryAdvisor as a bean? Is it proxied at some point? What happens if you instantiate it inline, as i have?

Alternatively: is it possible you don't have the Reactor libraries on the class path? I don't know..

  1. https://github.com/joshlong-attic/2025-05-29-spring-ai-graalvm-advisor-npe-3256

the good news is, as I just realized, is that most of the common BaseAdvisors are also final so they're likely not being proxied in the first place. which leaves me to wonder about Reactor, and all the other variables.

@dev-jonghoonpark
Copy link
Contributor Author

dev-jonghoonpark commented May 29, 2025

I reproduced the bug by following the code provided by the issue #3168.

Would you be able to test this repository?
https://github.com/dev-jonghoonpark/2025-05-29-spring-ai-graalvm-advisor-npe-3256

tested os:

macOS 14.2.1(23C71)

tested jdk:

openjdk 24.0.1 2025-04-15
OpenJDK Runtime Environment GraalVM CE 24.0.1+9.1 (build 24.0.1+9-jvmci-b01)
OpenJDK 64-Bit Server VM GraalVM CE 24.0.1+9.1 (build 24.0.1+9-jvmci-b01, mixed mode, sharing)

The error logs are displayed as follows:
error.log

There are no issues during normal execution, and there are no problems with the logic.
However, when executed using a native image, an unexpected issue occurs.

[Note]
The problem can be also resolved without modifying the code by using the --initialize-at-run-time=org.springframework.ai.chat.client.advisor.api.BaseAdvisor option during the native-image build process.

@joshlong @markpollack

@joshlong
Copy link
Member

joshlong commented May 30, 2025

that works! you're right, @dev-jonghoonpark - thank you very much

I have qualms about the design of BaseAdvisor. I think the getMethod() should return the Scheduler Factory and it should not be stored as a constant. But still, the code does work on the JVM, because on the JVM the class is loaded from bytes into memory and initialized in the same process.

GraalVM, otoh, wants to initialize as much as possible at compile time (sort of like evaluating C macros) so that when it comes to start the program, it can just load the memory straight in. no evaluating and crawling and whatever. So it makes most classes build-time init. This means an interface with a constant value would be fine in a graalvm context, but dynamic variables like the SchedulerFactory .. not so much.

the ugliest way ive found to fix this is to just check for null again during the MessageChatMemoryAdvisor.build call:

        /**
         * Build the advisor.
         * @return the advisor
         */
        public MessageChatMemoryAdvisor build() {
            if (this.scheduler == null) this.scheduler = BaseAdvisor.DEFAULT_SCHEDULER ;
            return new MessageChatMemoryAdvisor(this.chatMemory, this.conversationId, this.order, this.scheduler);
        }


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants