I use WebSocketStompClient with GlassFish web socket contained called Tyrus in the next way:

WebSocketStompClient client;

ClientManager clientManager = org.glassfish.tyrus.client.ClientManager.createClient();

clientManager.getProperties().put(
    SELECTOR_THREAD_POOL_CONFIG,
    defaultConfig().setPoolName("ws-selector"));

clientManager.getProperties().put(
    WORKER_THREAD_POOL_CONFIG,
    defaultConfig().setPoolName("ws-worker"));

client = new WebSocketStompClient(new StandardWebSocketClient(clientManager));

client.start();

sesion = client.connect(uri, handshakeHeaders(clusterSecret), connectHeaders(), sesHnd)
    .get(10L, SECONDS);

and after successful Web socket connection I receive an error message with 400 code.

org.gridgain.control.agent.ControlCenterAgent.AfterConnectedSessionHandler#handleTransportError of sesHnd object is executed after this error, but DefaultStompSession argument does not have the connection object inside. This connection object has type org.springframework.web.socket.messaging.WebSocketStompClient.WebSocketTcpConnectionHandlerAdapter and it’s stop method crear all servlet container resources on session.disconnect() in the common way.

But in my case I can see that some resources stay not released. Thread.getAllStackTraces() returns some ws-selector and ws-worker threads, and I cannot find the way to release them.

Used spring-core, spring-websocket, spring-messaging and spring-web 4.3.29.RELEASE, and tyrus-standalone-client 1.17

https://stackoverflow.com/questions/65108531/thread-leak-in-spring-websocketstompclient-with-integrated-glassfish-servlet-con

Comment From: rstoyanchev

It sounds like those resources are associated with the Tyrus ClientManager which you're initializing via client.start(), externally and all that StandardWebSocketClient is aware of is the WebSocketContainer being passed in. What are you expecting to happen in this case? Shouldn't there be some code that also stops the client at a certain point, possibly when the server is shut down?

Comment From: vsisko

I expect that in case of the handshake error WebSocketTcpConnectionHandlerAdapter or AbstractWebSocketSession will realise resources as it works in case of disconnect process. See the attached stacktrace:

shutdown:141, GrizzlyExecutorService (org.glassfish.grizzly.threadpool)
finalizeShutdown:628, NIOTransport (org.glassfish.grizzly.nio)
shutdownNow:592, NIOTransport (org.glassfish.grizzly.nio)
close:313, GrizzlyClientFilter$1 (org.glassfish.tyrus.container.grizzly.client)
close:607, TyrusClientEngine$2$1 (org.glassfish.tyrus.client)
execute:470, GrizzlyClientFilter$CloseTask (org.glassfish.tyrus.container.grizzly.client)
processTask:91, TaskProcessor (org.glassfish.tyrus.container.grizzly.client)
processTask:68, TaskProcessor (org.glassfish.tyrus.container.grizzly.client)
handleClose:197, GrizzlyClientFilter (org.glassfish.tyrus.container.grizzly.client)
execute:76, ExecutorResolver$4 (org.glassfish.grizzly.filterchain)
executeFilter:283, DefaultFilterChain (org.glassfish.grizzly.filterchain)
executeChainPart:200, DefaultFilterChain (org.glassfish.grizzly.filterchain)
execute:132, DefaultFilterChain (org.glassfish.grizzly.filterchain)
process:111, DefaultFilterChain (org.glassfish.grizzly.filterchain)
execute:77, ProcessorExecutor (org.glassfish.grizzly)
fireIOEvent:536, TCPNIOTransport (org.glassfish.grizzly.nio.transport)
preClose:836, NIOConnection (org.glassfish.grizzly.nio)
preClose:97, TCPNIOConnection (org.glassfish.grizzly.nio.transport)
terminate0:560, NIOConnection (org.glassfish.grizzly.nio)
terminate0:291, TCPNIOConnection (org.glassfish.grizzly.nio.transport)
completed:524, NIOConnection$3 (org.glassfish.grizzly.nio)
completed:520, NIOConnection$3 (org.glassfish.grizzly.nio)
notifyCompleteAndRecycle:173, AsyncWriteQueueRecord (org.glassfish.grizzly.asyncqueue)
write:279, AbstractNIOAsyncQueueWriter (org.glassfish.grizzly.nio)
write:169, AbstractNIOAsyncQueueWriter (org.glassfish.grizzly.nio)
write:71, AbstractNIOAsyncQueueWriter (org.glassfish.grizzly.nio)
write:76, AbstractWriter (org.glassfish.grizzly)
closeGracefully0:519, NIOConnection (org.glassfish.grizzly.nio)
closeSilently:495, NIOConnection (org.glassfish.grizzly.nio)
execute:160, GrizzlyWriter$CloseTask (org.glassfish.tyrus.container.grizzly.client)
processTask:91, TaskProcessor (org.glassfish.tyrus.container.grizzly.client)
processTask:68, TaskProcessor (org.glassfish.tyrus.container.grizzly.client)
close:115, GrizzlyWriter (org.glassfish.tyrus.container.grizzly.client)
close:308, GrizzlyClientFilter$1 (org.glassfish.tyrus.container.grizzly.client)
doClose:594, ProtocolHandler (org.glassfish.tyrus.core)
onClose:116, TyrusWebSocket (org.glassfish.tyrus.core)
close:481, ProtocolHandler (org.glassfish.tyrus.core)
close:244, TyrusWebSocket (org.glassfish.tyrus.core)
close:254, TyrusWebSocket (org.glassfish.tyrus.core)
close:480, TyrusRemoteEndpoint (org.glassfish.tyrus.core)
close:210, TyrusSession (org.glassfish.tyrus.core)
closeInternal:223, StandardWebSocketSession (org.springframework.web.socket.adapter.standard)
close:137, AbstractWebSocketSession (org.springframework.web.socket.adapter)
close:128, AbstractWebSocketSession (org.springframework.web.socket.adapter)
close:446, WebSocketStompClient$WebSocketTcpConnectionHandlerAdapter (org.springframework.web.socket.messaging)
resetConnection:496, DefaultStompSession (org.springframework.messaging.simp.stomp)
disconnect:355, DefaultStompSession (org.springframework.messaging.simp.stomp)

But now the resources stay allocated and i have no possibility to get access to WebSocketTcpConnectionHandlerAdapter or AbstractWebSocketSession directly and close them manually.

Comment From: rstoyanchev

StandardWebSocketClient accepts WebSocketContainer which does not expose any lifecycle methods and therefore StandardWebSocketClient does not know how to close those resources. Even if it did know that is a Tyrus ClientManager, generally as a rule we don't close resources that we didn't start ourselves. In this case your code invoked client.start() and we can't assume or know when you want to stop those resources since the same client could be re-used across different requests.

Note that the connect method returns ListenableFuture<StompSession> which will communicate failure to connect before the session is established.

Comment From: vsisko

I can see that WebSocketTcpConnectionHandlerAdapter#onFailure is notified and connection future throws an exception, but created for it StandardWebSocketSession in StandardWebSocketClient stay opened. Connection on server side stay alive until my client application is not stopped (or wait some timeout). Also i can see in Memory profiler of VisualVM many Spring object which are created in time of Stomp client connection and not available manually to stop or release by any way. GC does not remove them.

Comment From: rstoyanchev

StandardWebSocketClient does not hold on to any WebSocketSession instances. It simply returns it in a ListenableFuture so I don't see how the framework that could precluding GC of the session. As to why the connection on the server side remains alive, could be that the Tyrus client didn't properly close the connection? That brings up the point that you could plug in a different JSR-356 client in order to see how it behaves and narrow the problem down.

Comment From: vsisko

I use StandardWebSocketClient inside of WebSocketStompClient. It returns a future with StompSession which contains connection object of type WebSocketTcpConnectionHandlerAdapter with WebSocketSession inside. But 'connection' object is set only after successful handshake that in my case throw exception. I get neither the stomp session nor the web socket session. So i cannot through the stomp client close the opened web session by any way or find the way to close it.

Comment From: rstoyanchev

If the WebSocket handshake fails, there should be no WebSocketSession and WebSocketTcpConnectionHandlerAdapter#afterConnectionEstablished wouldn't be called.

Is it possible that the WebSocket handshake succeeds but there is a failure (soon) after that, before the STOMP CONNECTED frame is received which is when the STOMP session is ready for use? In other words in DefaultStompSession, the handleFailure method succeeds in setting the sessionFuture before the handleMessage does when the CONNECTED frame is received.

Comment From: rstoyanchev

I've scheduled this for 5.3.3 since I can see a potential issue with a WebSocket handshake succeeding (and hence getting passed the point of handleConnectFailure) but running into an error during the handling of the initial STOMP frame which could be reported to the ListenableFuture<StompSession> via handleFailure.

I'm going to experiment and confirm this but I'd also like to confirm if that is your scenario @vsisko? A potential fix could be to automatically close the session (and underlying connection) in that case since nothing further could be done with it.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: vsisko

Yes @rstoyanchev

As I described before the WebSocket connection is successfully established and WebSocketTcpConnectionHandlerAdapter#afterConnectionEstablished is executed. After this it sends the message

> Session dc6d1a99-92c9-4b88-899f-1ddb3ea9d538 [0 ms]: Sending handshake request:
> GET ws://localhost:38905/agents
> Agent-Version: 1.0.2
> Cluster-Id: f007ea5b-7c93-4b9f-af9b-d644c066d6b4
> Cluster-Secret: 25f54d6b-fa24-484b-ba31-7edddb2476fc
> Connection: Upgrade
> Host: localhost:38905
> Origin: http://localhost:38905
> Sec-WebSocket-Key: 1Gfx17xOHp8Lm0S8TrEDFg==
> Sec-WebSocket-Version: 13
> Upgrade: websocket

and receives the next response.

< Session dc6d1a99-92c9-4b88-899f-1ddb3ea9d538 [149 ms]: Received handshake response: 
< 400
< connection: close
< content-length: 0
< date: Sun, 27 Dec 2020 16:10:38 GMT

ListenableFuture\<StompSession> throws the next error on execution of get method:

javax.websocket.DeploymentException: Handshake error.

and after that I cannot find the way to close the WebSocket session and release it’s resources.

Comment From: rstoyanchev

This:

As I described before the WebSocket connection is successfully established

Contradicts the output showing the GET ws://localhost:38905/agents resulting in a 400 with "connection: close". A successful WebSocket connection results in a 101 (switching protocols) response status, and not a 400 (bad request). Furthermore "connection:close" should instruct the client to close the connection, so there should be nothing for you to close.

To try to reproduce this I created a server that returns 400. Then I used client code like this:

StompSessionHandler handler = new StompSessionHandlerAdapter() {
    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        logger.debug("after connected");
    }

    @Override
    public void handleException(StompSession s, StompCommand c, StompHeaders h, byte[] p, Throwable ex) {
        logger.error("exception", ex);
    }

    @Override
    public void handleTransportError(StompSession session, Throwable exception) {
        logger.error("transportError", exception);
    }
};

StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);

stompClient.connect("ws://localhost:8080/26213", handler)
        .addCallback(session -> logger.debug("Got session"), ex -> logger.error("Got error", ex));

This results in:

15:35:49.891 [SimpleAsyncTaskExecutor-1] ERROR org.springframework.samples.MyTests - Got error
javax.websocket.DeploymentException: The HTTP response from the server [400] did not permit the HTTP 
...
15:50:21.081 [SimpleAsyncTaskExecutor-1] ERROR org.springframework.samples.MyTests - transportError
javax.websocket.DeploymentException: The HTTP response from the server [400] did not permit the HTTP upgrade to WebSocket

When I break in handleTransportError I see this stack:

"SimpleAsyncTaskExecutor-1@2324" prio=5 tid=0xd nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at org.springframework.samples.portfolio.web.tomcat.MyTests$1.handleTransportError(MyTests.java:53)
      at org.springframework.messaging.simp.stomp.DefaultStompSession.afterConnectFailure(DefaultStompSession.java:408)
      at org.springframework.web.socket.messaging.WebSocketStompClient$WebSocketTcpConnectionHandlerAdapter.onFailure(WebSocketStompClient.java:317)
      at org.springframework.util.concurrent.ListenableFutureCallbackRegistry.notifyFailure(ListenableFutureCallbackRegistry.java:86)
      at org.springframework.util.concurrent.ListenableFutureCallbackRegistry.failure(ListenableFutureCallbackRegistry.java:158)
      - locked <0xb76> (a java.lang.Object)
      at org.springframework.util.concurrent.ListenableFutureTask.done(ListenableFutureTask.java:100)
      at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:384)
      at java.util.concurrent.FutureTask.setException(FutureTask.java:251)
      at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:271)
      at java.util.concurrent.FutureTask.run(FutureTask.java:-1)
      at java.lang.Thread.run(Thread.java:748)

In other words, for a server returning 400 from the handshake, WebSocketTcpConnectionHandlerAdapter#afterConnectionEstablished is never called because the handshake does not succeed. Instead this manifests as a connect failure and a session is never established.

On the other hand if as you say the connection is established successfully, i.e. including a 101 switching protocols status, then a subsequent 400 cannot happen because there can only be one response status for the handshake request.

At this point I don't understand the scenario. If you'd like us to look further, please provide a sample.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: snicoll

The bot should have closed this issue but didn't so I am going to do that. @vsisko if you can provide the details that Rossen requested, we can obviously reopen this issue.