Skip to content

Commit e15836e

Browse files
committed
Refactor timeout and close routines; seems to plug the container leak
1 parent 38d184b commit e15836e

File tree

6 files changed

+112
-73
lines changed

6 files changed

+112
-73
lines changed

server/src/main/java/org/red5/net/websocket/WebSocketConnection.java

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
2626
import java.util.stream.Stream;
2727

28+
import javax.websocket.CloseReason;
29+
import javax.websocket.CloseReason.CloseCode;
30+
import javax.websocket.CloseReason.CloseCodes;
2831
import javax.websocket.Extension;
2932
import javax.websocket.Session;
3033

@@ -65,6 +68,7 @@ public class WebSocketConnection extends AttributeStore implements Comparable<We
6568
// associated websocket session
6669
private final WsSession wsSession;
6770

71+
// reference to the scope for manager access
6872
private WeakReference<WebSocketScope> scope;
6973

7074
// unique identifier for the session
@@ -163,6 +167,8 @@ public WebSocketConnection(WebSocketScope scope, Session session) {
163167
// add the timeouts to the user props
164168
userProps.put(Constants.READ_IDLE_TIMEOUT_MS, readTimeout);
165169
userProps.put(Constants.WRITE_IDLE_TIMEOUT_MS, sendTimeout);
170+
// set the close timeout to 5 seconds
171+
userProps.put(Constants.SESSION_CLOSE_TIMEOUT_PROPERTY, TimeUnit.SECONDS.toMillis(5));
166172
if (isDebug) {
167173
log.debug("userProps: {}", userProps);
168174
}
@@ -186,9 +192,8 @@ public void send(String data) throws UnsupportedEncodingException, IOException {
186192
}
187193
// process the incoming string
188194
if (StringUtils.isNotBlank(data)) {
189-
final WsSession session = wsSession;
190195
// attempt send only if the session is not closed
191-
if (session != null && !session.isClosed()) {
196+
if (!wsSession.isClosed()) {
192197
try {
193198
if (useAsync) {
194199
if (sendFuture != null && !sendFuture.isDone()) {
@@ -197,21 +202,21 @@ public void send(String data) throws UnsupportedEncodingException, IOException {
197202
} catch (TimeoutException e) {
198203
log.warn("Send timed out {}", wsSessionId);
199204
// if the session is not open, cancel the future
200-
if (!session.isOpen()) {
205+
if (!wsSession.isOpen()) {
201206
sendFuture.cancel(true);
202207
return;
203208
}
204209
}
205210
}
206211
synchronized (wsSessionId) {
207212
int lengthToWrite = data.getBytes().length;
208-
sendFuture = session.getAsyncRemote().sendText(data);
213+
sendFuture = wsSession.getAsyncRemote().sendText(data);
209214
updateWriteBytes(lengthToWrite);
210215
}
211216
} else {
212217
synchronized (wsSessionId) {
213218
int lengthToWrite = data.getBytes().length;
214-
session.getBasicRemote().sendText(data);
219+
wsSession.getBasicRemote().sendText(data);
215220
updateWriteBytes(lengthToWrite);
216221
}
217222
}
@@ -236,8 +241,7 @@ public void send(byte[] buf) throws IOException {
236241
if (isDebug) {
237242
log.debug("send binary: {}", Arrays.toString(buf));
238243
}
239-
WsSession session = wsSession;
240-
if (session != null && session.isOpen()) {
244+
if (!wsSession.isClosed()) {
241245
try {
242246
// send the bytes
243247
if (useAsync) {
@@ -253,12 +257,12 @@ public void send(byte[] buf) throws IOException {
253257
}
254258
}
255259
synchronized (wsSessionId) {
256-
sendFuture = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(buf));
260+
sendFuture = wsSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(buf));
257261
updateWriteBytes(buf.length);
258262
}
259263
} else {
260264
synchronized (wsSessionId) {
261-
session.getBasicRemote().sendBinary(ByteBuffer.wrap(buf));
265+
wsSession.getBasicRemote().sendBinary(ByteBuffer.wrap(buf));
262266
updateWriteBytes(buf.length);
263267
}
264268
}
@@ -281,11 +285,10 @@ public void sendPing(byte[] buf) throws IllegalArgumentException, IOException {
281285
if (isTrace) {
282286
log.trace("send ping: {}", buf);
283287
}
284-
WsSession session = wsSession;
285-
if (session != null && session.isOpen()) {
288+
if (!wsSession.isClosed()) {
286289
synchronized (wsSessionId) {
287290
// send the bytes
288-
session.getBasicRemote().sendPing(ByteBuffer.wrap(buf));
291+
wsSession.getBasicRemote().sendPing(ByteBuffer.wrap(buf));
289292
// update counter
290293
updateWriteBytes(buf.length);
291294
}
@@ -305,11 +308,10 @@ public void sendPong(byte[] buf) throws IllegalArgumentException, IOException {
305308
if (isTrace) {
306309
log.trace("send pong: {}", buf);
307310
}
308-
WsSession session = wsSession;
309-
if (session != null && session.isOpen()) {
311+
if (!wsSession.isClosed()) {
310312
synchronized (wsSessionId) {
311313
// send the bytes
312-
session.getBasicRemote().sendPong(ByteBuffer.wrap(buf));
314+
wsSession.getBasicRemote().sendPong(ByteBuffer.wrap(buf));
313315
// update counter
314316
updateWriteBytes(buf.length);
315317
}
@@ -319,14 +321,34 @@ public void sendPong(byte[] buf) throws IllegalArgumentException, IOException {
319321
}
320322

321323
/**
322-
* close Connection
324+
* Close the connection.
323325
*/
324326
public void close() {
327+
close(CloseCodes.NORMAL_CLOSURE, "");
328+
}
329+
330+
/**
331+
* Close the connection with a reason.
332+
*
333+
* @param code CloseCode
334+
* @param reasonPhrase short reason for closing
335+
*/
336+
public void close(CloseCode code, String reasonPhrase) {
325337
if (connected.compareAndSet(true, false)) {
326-
log.debug("close: {}", wsSessionId);
327-
// trying to close the session nicely
338+
// no blank reasons
339+
if (reasonPhrase == null) {
340+
reasonPhrase = "";
341+
}
342+
log.debug("close: {} code: {} reason: {}", wsSessionId, code, reasonPhrase);
328343
try {
329-
wsSession.close();
344+
// close the session if open
345+
if (wsSession.isOpen()) {
346+
CloseReason reason = new CloseReason(code, reasonPhrase);
347+
if (isDebug) {
348+
log.debug("Closing session: {} with reason: {}", wsSessionId, reason);
349+
}
350+
wsSession.close(reason);
351+
}
330352
} catch (Exception e) {
331353
log.debug("Exception closing session", e);
332354
}
@@ -343,39 +365,9 @@ public void close() {
343365
if (headers != null) {
344366
headers = null;
345367
}
346-
if (scope.get() != null) {
347-
// disconnect from scope
348-
scope.get().removeConnection(this);
349-
// clear weak refs
350-
scope.clear();
351-
}
352368
}
353369
}
354370

355-
/*
356-
WsSession uses these userProperties for checkExpiration along with maxIdleTimeout
357-
358-
configuration for read idle timeout on WebSocket session
359-
READ_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.READ_IDLE_TIMEOUT_MS";
360-
configuration for write idle timeout on WebSocket session
361-
WRITE_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.WRITE_IDLE_TIMEOUT_MS";
362-
*/
363-
public void timeoutAsync(long now) {
364-
// XXX(paul) only logging here as we should more than likely rely upon the container checking expiration
365-
log.trace("timeoutAsync: {} on session id: {} read: {} written: {}", now, wsSessionId, readBytes, writtenBytes);
366-
/*
367-
WsSession session = wsSession;
368-
Map<String, Object> props = session.getUserProperties();
369-
log.debug("Session properties: {}", props);
370-
long maxIdleTimeout = session.getMaxIdleTimeout();
371-
long readTimeout = (long) props.get(Constants.READ_IDLE_TIMEOUT_MS);
372-
long sendTimeout = (long) props.get(Constants.WRITE_IDLE_TIMEOUT_MS);
373-
log.debug("Session timeouts - max: {} read: {} write: {}", maxIdleTimeout, readTimeout, sendTimeout);
374-
//long readDelta = (now - lastReadTime), writeDelta = (now - lastWriteTime);
375-
//log.debug("timeoutAsync: {} on {} last read: {} last write: {}", now, wsSessionId, readDelta, writeDelta);
376-
*/
377-
}
378-
379371
/**
380372
* Async send is enabled in non-Windows based systems; this provides a means to override it.
381373
*

server/src/main/java/org/red5/net/websocket/WebSocketScope.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.util.concurrent.ConcurrentSkipListSet;
1515
import java.util.concurrent.CopyOnWriteArraySet;
1616

17+
import javax.websocket.CloseReason.CloseCodes;
18+
1719
import org.red5.net.websocket.listener.IWebSocketDataListener;
1820
import org.red5.net.websocket.model.WSMessage;
1921
import org.red5.server.api.scope.IScope;
@@ -91,7 +93,7 @@ public void unregister() {
9193
// clean up the connections by first closing them
9294
conns.forEach(conn -> {
9395
if (conns.remove(conn)) {
94-
conn.close();
96+
conn.close(CloseCodes.GOING_AWAY, "WebSocket scope removed");
9597
}
9698
});
9799
// clean up the listeners by first stopping them

server/src/main/java/org/red5/net/websocket/WebSocketScopeManager.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import java.util.concurrent.Executors;
1616
import java.util.concurrent.Future;
1717

18+
import javax.websocket.CloseReason.CloseCodes;
19+
1820
import org.red5.net.websocket.listener.DefaultWebSocketDataListener;
1921
import org.red5.net.websocket.listener.IWebSocketDataListener;
2022
import org.red5.net.websocket.listener.IWebSocketScopeListener;
@@ -172,17 +174,12 @@ public boolean addWebSocketScope(WebSocketScope webSocketScope) {
172174
wsConn.sendPing(PING_BYTES);
173175
} catch (Exception e) {
174176
log.debug("Exception pinging connection: {} connection will be closed", wsConn.getSessionId(), e);
175-
// if the connection isn't connected, remove them
176-
wsScope.removeConnection(wsConn);
177-
// if the ping fails, consider them gone
178-
wsConn.close();
177+
wsConn.close(CloseCodes.CLOSED_ABNORMALLY, e.getMessage());
179178
}
180179
} else {
181180
log.debug("Removing unconnected connection: {} during ping loop", wsConn.getSessionId());
182181
// if the connection isn't connected, remove them
183-
wsScope.removeConnection(wsConn);
184-
// if connection is not connected, close it (ensure closed / removed)
185-
wsConn.close();
182+
wsConn.close(CloseCodes.UNEXPECTED_CONDITION, "Connection not connected");
186183
}
187184
} catch (Exception e) {
188185
log.warn("Exception in WS pinger", e);

server/src/main/java/org/red5/net/websocket/server/DefaultWebSocketEndpoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public void onClose(Session session, CloseReason closeReason) {
9191
// force remove on exception
9292
scope.removeConnection(conn);
9393
// fire close, to be sure
94-
conn.close();
94+
conn.close(closeReason.getCloseCode(), closeReason.getReasonPhrase());
9595
}
9696
}
9797
}

server/src/main/java/org/red5/net/websocket/server/DefaultWsServerContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ protected void registerSession(Object endpoint, WsSession wsSession) {
301301
*/
302302
@Override
303303
protected void unregisterSession(Object endpoint, WsSession wsSession) {
304-
if (wsSession.getUserPrincipal() != null && wsSession.getHttpSessionId() != null) {
304+
if (wsSession.getHttpSessionId() != null) {
305305
unregisterAuthenticatedSession(wsSession, wsSession.getHttpSessionId());
306306
log.debug("unregisterSession - unregisterAuthenticatedSession: {}", wsSession.getId());
307307
}

server/src/main/java/org/red5/net/websocket/server/WsHttpUpgradeHandler.java

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.tomcat.util.net.SocketEvent;
2222
import org.apache.tomcat.util.net.SocketWrapperBase;
2323
import org.apache.tomcat.util.res.StringManager;
24+
import org.apache.tomcat.websocket.Constants;
2425
import org.apache.tomcat.websocket.Transformation;
2526
import org.apache.tomcat.websocket.WsIOException;
2627
import org.apache.tomcat.websocket.WsSession;
@@ -78,6 +79,10 @@ public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
7879

7980
private WsSession wsSession;
8081

82+
private long lastTimeoutCheck = System.currentTimeMillis();
83+
84+
private long lastReadBytes, lastWrittenBytes;
85+
8186
public WsHttpUpgradeHandler() {
8287
applicationClassLoader = Thread.currentThread().getContextClassLoader();
8388
}
@@ -284,23 +289,66 @@ public void setSslSupport(SSLSupport sslSupport) {
284289
@Override
285290
public void timeoutAsync(long now) {
286291
log.trace("timeoutAsync: {} on session: {}", now, wsSession);
287-
// session methods may not be called if the session is not open
288292
if (wsSession != null) {
289-
if (wsSession.isOpen()) {
290-
try {
291-
// if we have a timeout, inform the ws connection
292-
WebSocketConnection conn = (WebSocketConnection) wsSession.getUserProperties().get(WSConstants.WS_CONNECTION);
293-
if (conn != null) {
294-
conn.timeoutAsync(now);
293+
try {
294+
final String wsSessionId = wsSession.getId();
295+
// get scope from endpoint config
296+
WebSocketScope scope = (WebSocketScope) endpointConfig.getUserProperties().get(WSConstants.WS_SCOPE);
297+
// do lookup by session id, skips need for session user props
298+
WebSocketConnection conn = scope.getConnectionBySessionId(wsSessionId);
299+
// if we don't get it from the scope, try the session lookup
300+
if (conn == null && wsSession.isOpen()) {
301+
// session methods may not be called if its not open
302+
conn = (WebSocketConnection) wsSession.getUserProperties().get(WSConstants.WS_CONNECTION);
303+
}
304+
// last check, if we don't have a connection, log a warning
305+
if (conn == null) {
306+
log.warn("Connection for id: {} was not found in the scope or session: {}", wsSession.getId(), scope.getPath());
307+
return;
308+
}
309+
// negative now means always treat as expired
310+
if (now > 0) {
311+
long checkDelta = now - lastTimeoutCheck;
312+
long readBytes = conn.getReadBytes(), writtenBytes = conn.getWrittenBytes();
313+
log.info("timeoutAsync: {}ms on session id: {} read: {} written: {}", checkDelta, wsSessionId, readBytes, writtenBytes);
314+
Map<String, Object> props = wsSession.getUserProperties();
315+
log.debug("Session properties: {}", props);
316+
long maxIdleTimeout = wsSession.getMaxIdleTimeout();
317+
long readTimeout = (long) props.get(Constants.READ_IDLE_TIMEOUT_MS);
318+
long writeTimeout = (long) props.get(Constants.WRITE_IDLE_TIMEOUT_MS);
319+
log.debug("Session timeouts - max: {} read: {} write: {}", maxIdleTimeout, readTimeout, writeTimeout);
320+
if (maxIdleTimeout > 0) {
321+
if (checkDelta > maxIdleTimeout && (readBytes == lastReadBytes || writtenBytes == lastWrittenBytes)) {
322+
log.info("Max idle timeout: {}ms on session id: {}", checkDelta, wsSessionId);
323+
conn.close(CloseCodes.GOING_AWAY, "Max idle timeout");
324+
}
325+
} else {
326+
if (readTimeout > 0) {
327+
if (readBytes == lastReadBytes) {
328+
if (checkDelta > readTimeout) {
329+
log.info("Read timeout: {}ms on session id: {}", checkDelta, wsSessionId);
330+
conn.close(CloseCodes.GOING_AWAY, "Read timeout");
331+
}
332+
}
333+
}
334+
if (writeTimeout > 0) {
335+
if (writtenBytes == lastWrittenBytes) {
336+
if (checkDelta > writeTimeout) {
337+
log.info("Write timeout: {}ms on session id: {}", checkDelta, wsSessionId);
338+
conn.close(CloseCodes.GOING_AWAY, "Write timeout");
339+
}
340+
}
341+
}
295342
}
296-
} catch (Throwable t) {
297-
log.warn(sm.getString("wsHttpUpgradeHandler.timeoutAsyncFailed"), t);
343+
lastReadBytes = readBytes;
344+
lastWrittenBytes = writtenBytes;
345+
lastTimeoutCheck = now;
346+
} else {
347+
log.warn("timeoutAsync: negative time on session id: {}", wsSessionId);
348+
conn.close(CloseCodes.GOING_AWAY, "Timeout expired");
298349
}
299-
} else {
300-
log.debug("timeoutAsync: {} session is not open for session id: {}", now, wsSession.getId());
301-
// we need the processor released from the async waitingProcessors list
302-
// located in abstract protocol
303-
//socketWrapper.close();
350+
} catch (Throwable t) {
351+
log.warn(sm.getString("wsHttpUpgradeHandler.timeoutAsyncFailed"), t);
304352
}
305353
}
306354
}

0 commit comments

Comments
 (0)