spring-cloud / spring-cloud-stream

Framework for building Event-Driven Microservices
http://cloud.spring.io/spring-cloud-stream
Apache License 2.0
1.01k stars 613 forks source link

Need For Specifiying All The Binders During Build Time In Sping Native #2804

Closed omercelikceng closed 1 year ago

omercelikceng commented 1 year ago

Hello,

I have a spring application running using Spring Cloud Stream Function(Spring Native). I am consuming some data using cloud function. I generated a native docker image with spring-boot-maven-plugin.

Additionally, I read spring cloud stream properties from config-server. This is exactly where my problem starts. Actually, I have no problem reading properties from config-server. However, for native (AOT), it seems like I have to specify spring cloud stream binders at build time. But I want to get the binder configurations from config server during application startup as I have been doing before native.

Case 1:

If I configure the application to connect to the config-server while building the project, the code runs without any problem.

Related application.yml

spring:
  application:
    name: person-service
  cloud:
    config:
      enabled: true
  config:
    import: optional:configserver:http://config-server

What I see in your code is that If I build as shown above (fetching from config server during build time), the following classes are generated for all binders correctly. Then, afterwards there is no problem in getting these classes from applicationContext in runtime and use them(DefaultBinderFactory - getBinderInstance- this.binderChildContextInitializers.get(configurationName).initialize(binderProducingContext)). But unfortunately, I don't have the chance to access config server in build time. I did this for testing purposes. I can't work like this in Production.

Case 2:

spring:
  application:
    name: person-service
  cloud:
    config:
      enabled: false

Another option is to disable property reading from the config server at build time.
In this case, I enable config-server from docker-compose.yml to fetch the properties from the config server at runtime. If I do it this way, the relevant classes for the binders are not generated that are supposed to be generated in build time. Then I get an error because these binders cannot be found in the runtime.


Case 1 - Classes Generated After Code Build (Config.Enabled = True) image


Case 2 - Classes Generated After Code Build (Config.Enabled = False) combined

I am getting the error here. Because the binderChildContextInitializers instance in DefaultBinderFactory was not initialized with the relevant binders at build time. It seems like this is a must to fill the binderChildContextInitializers in native build.

Error :

org.springframework.context.ApplicationContextException: Failed to start bean 'inputBindingLifecycle'
        at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:182) ~[spring-context-6.0.11.jar!/:6.0.11]
        at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:357) ~[spring-context-6.0.11.jar!/:6.0.11]
        at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
        at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:156) ~[spring-context-6.0.11.jar!/:6.0.11]
        at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:124) ~[spring-context-6.0.11.jar!/:6.0.11]
        at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:958) ~[spring-context-6.0.11.jar!/:6.0.11]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:611) ~[spring-context-6.0.11.jar!/:6.0.11]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:436) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.1.2.jar!/:3.1.2]
        at org.cloud.framework.service.milestone.app.MilestoneServiceApplication.main(MilestoneServiceApplication.java:20) ~[classes!/:6.0.0-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[milestone-service-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:95) ~[milestone-service-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[milestone-service-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:65) ~[milestone-service-6.0.0-SNAPSHOT.jar:6.0.0-SNAPSHOT]
Caused by: java.lang.IllegalStateException: Requested binder 'commonBinder' did not match available binders: [kafka]
        at org.springframework.cloud.stream.binder.DefaultBinderFactory.doGetBinderAOT(DefaultBinderFactory.java:206) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binder.DefaultBinderFactory.doGetBinder(DefaultBinderFactory.java:178) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binder.DefaultBinderFactory.getBinder(DefaultBinderFactory.java:167) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binding.BindingService.getBinder(BindingService.java:411) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binding.BindingService.bindConsumer(BindingService.java:108) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binding.AbstractBindableProxyFactory.createAndBindInputs(AbstractBindableProxyFactory.java:98) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binding.InputBindingLifecycle.doStartWithBindable(InputBindingLifecycle.java:67) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at java.base/java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:647) ~[na:na]
        at org.springframework.cloud.stream.binding.AbstractBindingLifecycle.start(AbstractBindingLifecycle.java:57) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.cloud.stream.binding.InputBindingLifecycle.start(InputBindingLifecycle.java:38) ~[spring-cloud-stream-4.1.1-SNAPSHOT.jar!/:4.1.1-SNAPSHOT]
        at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:179) ~[spring-context-6.0.11.jar!/:6.0.11]
        ... 21 common frames omitted

I kind of get that these should be initialized during build time but how can we get over this situation (fetching binders during application startup)? Recently, I have been opening pull requests for problems caused by Spring Native. If you direct me, I can make the development and open a pull request.

sobychacko commented 1 year ago

@omercelikceng This is indeed a tricky one. AOT requires these need to be set at build time. The generated output is based on the config at build time. Applying such optimizations early implies the following restrictions:

See this for more info: https://docs.spring.io/spring-framework/reference/core/aot.html#aot.bestpractices Thanks, @onobc, for pointing this out.

Moreover, this is not just a config-server-only issue. Any property (via CLI, env variables, etc.) that changes the application context at runtime will have the same issue.

After chatting with the team internally, we concluded that this is certainly a restriction in place by the AOT engine. The best we can recommend is putting as much config as possible at build time for initializing the binder context. This is not ideal, but please let us know how it goes.

sobychacko commented 1 year ago

See this too: https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_aot_and_native_image_support_2

omercelikceng commented 1 year ago

Thank you very much for your explanations @sobychacko . I try to contribute to your community as much as I can. I want to say some things. Actually, we can fetching configuration from config-server. Wouldn't it be more useful if we went for another solution instead of creating a class for each binder in your source code? Since I don't know the code as much as you do, I might be giving a ridiculous idea. I understand the issue is related to AOT. But I thought we could solve it by making changes to the code. I will also try to spend more time review into your code.

sobychacko commented 1 year ago

Wouldn't it be more useful if we went for another solution instead of creating a class for each binder in your source code?

Not sure I follow. In Spring Cloud Stream, we create child contexts for the binders in order to support multi-binder-based applications. This is fundamental to the binder bootstrapping architecture and hard to change. Is this what you meant?

omercelikceng commented 1 year ago

Yes. I guess the code architecture is hard to change. However, it will be very difficult for developers to build the code for every change I make in the configuration (Example: topicName). I think it is important to find a solution to this issue.

I'm not as knowledgeable about the subject as you are. So forgive me if I have any funny thoughts.

sobychacko commented 1 year ago

No worries. Let us chat more so that we can understand your requirements. Are we still talking about the original concerns raised in this issue, i.e., properties provided via config-server and therefore it cannot work at runtime in an AOT context?

omercelikceng commented 1 year ago

Yes. Let me try to explain to you what I tried.

There is no problem in fetching data from the Config server at runtime in a spring native project.

However, in a native spring project, spring cloud obliges me to specify stream binders at build time. This time, I will have to rebuild the code every time the binder configuration changes. This is a very difficult development process for me. I'm wondering if the binder creation mechanism can be modified to find a solution to this.

Also, I didn't look too closely. Do I need to specify other configurations at build time? For example; Do configurations under bindings need to be specified at build time?

sobychacko commented 1 year ago

All information that needs to be run in AOT and native mode must be provided at the build time so that the AOT engine can build the context. Please take a look at the docs I linked above for details. Any Spring context modifying operations are not permitted when running AOT mode as everything needed to run in AOT mode is already created at build time.

eomercelik commented 1 year ago

Thank you @sobychacko. I will do more research on native and read the resources you provided. Maybe I'll come back after I learn more. 😊

eomercelik commented 1 year ago

@sobychacko Would you like me to close the issue because I could not explain the problem clearly or for a similar reason?

sobychacko commented 1 year ago

@omercelikceng If you want to close the issue, that is fine. I think you explained the problem really well. The thing is that because of the AOT limitations, it is very hard to achieve a solution in this case. If you want to close this issue and ponder on an alternative solution, that is fine with us. We can always link this issue if you send a PR related to this.

eomercelik commented 1 year ago

@sobychacko I'm glad I was able to explain the problem well. I will work on this and to open a pull request. Thanks for be interested.

omercelikceng commented 1 year ago

@sobychacko . I re-read all of our conversations today. I realized that I couldn't explain myself well. If it's okay for you, I'll try to open a pull request.

What I am trying to suggest is; we are using spring prototype beans for these cases. I know that for AOT runtime code generation is not possible. But instead of generating specific classes for each binder at the build time, if we use a generic prototype bean and create a different instance for each binder during runtime, it will be both suitable for AOT and JIT. By this way, may be the fundamental bootstrapping architecture will not be changed too much, and still we have different instances for binders at the runtime.

olegz commented 1 year ago

@omercelikceng the PRs are always welcomed. so you don't have to ask ;)

But based on my understanding of what you are proposing, it will be a fundamental change in the architecture of s-c-stream as @sobychacko pointed out earlier. Regardless, going through this effort would probably help you as well to understand what we are dealing with.

That said, there are few things that the greater s-c-cloud team is working on, specifically to deal with AOT and Config server.

Feel free to close this issue if you want to

omercelikceng commented 1 year ago

Got it @olegz. Thank you very much to both you and @sobychacko for your explanations. You know the project better than me and you have much better experience than me. I have infinite respect for you, @sobychacko and spring cloud stream team. I just wanted to talk about this issue because I think it's important. But I appreciate your suggestions and will definitely take into consideration. Instead of spending effort on this issue, I will try to solve other issues. Thank you very much again.