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.