spring-integration icon indicating copy to clipboard operation
spring-integration copied to clipboard

Application shuts down immediately after connecting to a websocket

Open daniel-oelert opened this issue 1 year ago • 7 comments

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 avatar Sep 13 '24 12:09 daniel-oelert

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 avatar Sep 20 '24 18:09 artembilan

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

daniel-oelert avatar Sep 22 '24 08:09 daniel-oelert

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.

artembilan avatar Sep 22 '24 12:09 artembilan

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

daniel-oelert avatar Sep 24 '24 16:09 daniel-oelert

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);
           };
        }
    }

artembilan avatar Sep 24 '24 17:09 artembilan

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

daniel-oelert avatar Sep 28 '24 10:09 daniel-oelert

Reopen after revert (https://github.com/spring-projects/spring-integration/commit/2cf2f109f97e23f7af9dc07f14e6b701d6da1018). 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 avatar Sep 30 '24 17:09 artembilan