Skip to content
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

Application shuts down immediately after connecting to a websocket #9455

Open
daniel-oelert opened this issue Sep 13, 2024 · 7 comments · Fixed by #9493
Open

Application shuts down immediately after connecting to a websocket #9455

daniel-oelert opened this issue Sep 13, 2024 · 7 comments · Fixed by #9493

Comments

@daniel-oelert
Copy link

I wrote a simple Spring Integration app that connects to a websocket, using spring-integration-websocket.

Expected Behavior

The app connects to the websocket and keeps listening for new messages.

Current Behavior

Once connected the application shuts down immediately, since there is no non-daemon thread running, as pointed out by @artembilan on stackoverflow (https://stackoverflow.com/questions/78961574/why-does-my-spring-integration-app-just-shutdown-without-listening/) and the canonical way to keep this from happening is to use the property spring.main.keep-alive=true.

Context

It seems like a strange convention to keep this as the default. I think it would make sense to make the default the inverse of the current behavior.

A working example:

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.websocket.ClientWebSocketContainer;
import org.springframework.integration.websocket.inbound.WebSocketInboundChannelAdapter;
import org.springframework.messaging.MessageChannel;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;

@SpringBootApplication
@EnableIntegration
public class BinanceListenerApplication {

    @Bean
    MessageChannel rawAggTradeChannel(){
        return new PublishSubscribeChannel();
    }

    @Bean
    IntegrationFlow logging(){
        return IntegrationFlow.from(rawAggTradeChannel()).handle(new LoggingHandler(LoggingHandler.Level.INFO)).get();
    }

    @Bean
    IntegrationFlow binanceWebsocketAggTradeStream() {
        var clientWebSocketContainer = new ClientWebSocketContainer(new StandardWebSocketClient(),
                "wss://stream.binance.com:9443/ws/avaxusdt@aggTrade");
        var websocketInboundChannelAdapter = new WebSocketInboundChannelAdapter(clientWebSocketContainer);
        return IntegrationFlow
                .from(websocketInboundChannelAdapter)
                .channel(rawAggTradeChannel())
                .get();
    }

    public static void main(String[] args) {

        SpringApplication application = new SpringApplication(BinanceListenerApplication.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

}
@daniel-oelert daniel-oelert added status: waiting-for-triage The issue need to be evaluated and its future decided type: enhancement labels Sep 13, 2024
@artembilan artembilan added this to the 6.4.0-RC1 milestone Sep 19, 2024
@artembilan artembilan added in: core and removed status: waiting-for-triage The issue need to be evaluated and its future decided labels Sep 19, 2024
@artembilan artembilan self-assigned this Sep 19, 2024
@artembilan
Copy link
Member

Hi @daniel-oelert !

Would you mind confirming that you still use Java 17?
Or that is only a problem starting with Java 21 and enabled virtual threads for your Spring Boot application?
Thanks

artembilan added a commit that referenced this issue Sep 20, 2024
Fixes: #9455
Issue link: #9455

* Add an `IntegrationKeepAlive` infrastructure bean to initiate a long-lived non-daemon thread
to keep application alive when it cannot be kept like that for various reason, but has to.
* Expose `spring.integration.keepAlive` global property to disable an `IntegrationKeepAlive` auto-startup
* Test and document the feature
@daniel-oelert
Copy link
Author

I created the project with Spring Initializr and Java 21. No explicit virtual threads configuration was used.

@artembilan
Copy link
Member

Can you share such a simple project with us? I mentioned WebSocket client in my fix, but my impression might be false and we really may fail to keep alive only when virtual threads are there.

@daniel-oelert
Copy link
Author

Sure! I have attached a zip of a minimal project. Let me know if that is what you needed.
example_project.zip

@artembilan
Copy link
Member

Thank you for the sample!
I had to fix it like this:

"wss://fstream.binance.com/ws/bnbusdt@aggTrade"

to make it emitting data when spring.main.keep-alive=true, but I see what is going on.
The StandardWebSocketClient uses a default SimpleAsyncTaskExecutor for connection.
But then when WebSocket session is created, it is managed by Tomcat's WsWebSocketContainer.
And that one in the end hands everything to the AsyncChannelWrapperSecure which uses this one:

    private static class SecureIOThreadFactory implements ThreadFactory {

        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("WebSocketClient-SecureIO-" + count.incrementAndGet());
            // No need to set the context class loader. The threads will be
            // cleaned up when the connection is closed.
            t.setDaemon(true);
            return t;
        }
    }

So, our non-daemon SimpleAsyncTaskExecutor has done its job with connection task, but AsyncChannelWrapperSecure deals with daemon threads from now on.
Therefore my conclusion in the doc for fix on this issue is correct and indeed if we don't have any other background tasks, our application exits because all is left are those daemon threads for WebSocket sessions.

And apparently ThreadPool.defaultThreadFactory() also does daemons (in case of non-SSL WebSocket connection):

    static ThreadFactory defaultThreadFactory() {
        if (System.getSecurityManager() == null) {
            return (Runnable r) -> {
                Thread t = new Thread(r);
                t.setDaemon(true);
                return t;
            };
        } else {
            return (Runnable r) -> {
                PrivilegedAction<Thread> action = () -> {
                    Thread t = InnocuousThread.newThread(r);
                    t.setDaemon(true);
                    return t;
               };
               return AccessController.doPrivileged(action);
           };
        }
    }

@daniel-oelert
Copy link
Author

Thank you for the help and especially for the quick response!

@artembilan
Copy link
Member

Reopen after revert (2cf2f10).
See linked Spring Boot issue: we will reconsider the feature to something more generic or more flexible in the future release.
A general idea is to not have too many keep-alive choices.

For now the spring.main.keep-alive=true is the way to go if you see the application stopping prematurely, when it not supposed by the logic.

Sorry for inconvenience.

@artembilan artembilan reopened this Sep 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants