This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit a37a6d312c4bfa7e46e08266505b3c26413ec2a2
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Thu May 2 15:13:53 2024 +0100

    Fix BZ 68884 - improve handling of large scale WebSocket disconnects
---
 java/org/apache/tomcat/websocket/Constants.java    |  5 +++++
 .../tomcat/websocket/WsRemoteEndpointImplBase.java | 23 +++++++++++++---------
 java/org/apache/tomcat/websocket/WsSession.java    | 23 +++++++++++++++++++++-
 webapps/docs/changelog.xml                         | 12 +++++++++++
 webapps/docs/web-socket-howto.xml                  | 14 +++++++++++--
 5 files changed, 65 insertions(+), 12 deletions(-)

diff --git a/java/org/apache/tomcat/websocket/Constants.java 
b/java/org/apache/tomcat/websocket/Constants.java
index d03e21abc8..3ea477b26e 100644
--- a/java/org/apache/tomcat/websocket/Constants.java
+++ b/java/org/apache/tomcat/websocket/Constants.java
@@ -113,6 +113,11 @@ public class Constants {
     // Default is 30 seconds - setting is in milliseconds
     public static final long DEFAULT_SESSION_CLOSE_TIMEOUT = 
TimeUnit.SECONDS.toMillis(30);
 
+    // Configuration for session close timeout
+    public static final String ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT_PROPERTY = 
"org.apache.tomcat.websocket.ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT";
+    // Default is 50 milliseconds - setting is in milliseconds
+    public static final long DEFAULT_ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT = 50;
+
     // Configuration for read idle timeout on WebSocket session
     public static final String READ_IDLE_TIMEOUT_MS = 
"org.apache.tomcat.websocket.READ_IDLE_TIMEOUT_MS";
 
diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java 
b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
index 9008db20d6..f366ae20c5 100644
--- a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
+++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
@@ -236,7 +236,7 @@ public abstract class WsRemoteEndpointImplBase implements 
RemoteEndpoint {
 
 
     void sendMessageBlock(CharBuffer part, boolean last) throws IOException {
-        long timeoutExpiry = getTimeoutExpiry();
+        long timeout = getBlockingSendTimeout();
         boolean isDone = false;
         while (!isDone) {
             encoderBuffer.clear();
@@ -246,22 +246,27 @@ public abstract class WsRemoteEndpointImplBase implements 
RemoteEndpoint {
             }
             isDone = !cr.isOverflow();
             encoderBuffer.flip();
-            sendMessageBlock(Constants.OPCODE_TEXT, encoderBuffer, last && 
isDone, timeoutExpiry);
+            sendMessageBlock(Constants.OPCODE_TEXT, encoderBuffer, last && 
isDone, timeout);
         }
         stateMachine.complete(last);
     }
 
 
     void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last) 
throws IOException {
-        sendMessageBlock(opCode, payload, last, getTimeoutExpiry());
+        sendMessageBlock(opCode, payload, last, getBlockingSendTimeout());
     }
 
 
-    private long getTimeoutExpiry() {
-        // Get the timeout before we send the message. The message may
-        // trigger a session close and depending on timing the client
-        // session may close before we can read the timeout.
-        long timeout = getBlockingSendTimeout();
+    void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last, long 
timeout) throws IOException {
+        /*
+         *  Get the timeout before we send the message. The message may 
trigger a session close and depending on timing
+         *  the client session may close before we can read the timeout.
+         */
+        sendMessageBlockInternal(opCode, payload, last, 
getTimeoutExpiry(timeout));
+    }
+
+
+    private long getTimeoutExpiry(long timeout) {
         if (timeout < 0) {
             return Long.MAX_VALUE;
         } else {
@@ -270,7 +275,7 @@ public abstract class WsRemoteEndpointImplBase implements 
RemoteEndpoint {
     }
 
 
-    private void sendMessageBlock(byte opCode, ByteBuffer payload, boolean 
last, long timeoutExpiry)
+    private void sendMessageBlockInternal(byte opCode, ByteBuffer payload, 
boolean last, long timeoutExpiry)
             throws IOException {
         wsSession.updateLastActiveWrite();
 
diff --git a/java/org/apache/tomcat/websocket/WsSession.java 
b/java/org/apache/tomcat/websocket/WsSession.java
index e71b719e27..8892e7d960 100644
--- a/java/org/apache/tomcat/websocket/WsSession.java
+++ b/java/org/apache/tomcat/websocket/WsSession.java
@@ -771,6 +771,22 @@ public class WsSession implements Session {
     }
 
 
+    /*
+     * Returns the session close timeout in milliseconds
+     */
+    private long getAbnormalSessionCloseSendTimeout() {
+        long result = 0;
+        Object obj = 
userProperties.get(Constants.ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT_PROPERTY);
+        if (obj instanceof Long) {
+            result = ((Long) obj).longValue();
+        }
+        if (result <= 0) {
+            result = Constants.DEFAULT_ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT;
+        }
+        return result;
+    }
+
+
     protected void checkCloseTimeout() {
         // Skip the check if no session close timeout has been set.
         if (sessionCloseTimeoutExpiry != null) {
@@ -850,7 +866,12 @@ public class WsSession implements Session {
         }
         msg.flip();
         try {
-            wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, 
true);
+            if (closeCode == CloseCodes.NORMAL_CLOSURE) {
+                wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, 
true);
+            } else {
+                wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, 
true,
+                        getAbnormalSessionCloseSendTimeout());
+            }
         } catch (IOException | IllegalStateException e) {
             // Failed to send close message. Close the socket and let the 
caller
             // deal with the Exception
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 7f523a580f..132a9a517c 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -194,6 +194,18 @@
       </add>
     </changelog>
   </subsection>
+  <subsection name="WebSocket">
+    <changelog>
+      <fix>
+        <bug>68884</bug>: Reduce the write timeout when writing WebSocket close
+        messages for abnormal closes. The timeout defaults to 50 milliseconds
+        and may be controlled using the
+        
<code>org.apache.tomcat.websocket.ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT</code>
+        property in the user properties collection associated with the 
WebSocket
+        session. (markt)
+      </fix>
+    </changelog>
+  </subsection>
   <subsection name="Web applications">
     <changelog>
       <fix>
diff --git a/webapps/docs/web-socket-howto.xml 
b/webapps/docs/web-socket-howto.xml
index 7eda457be2..e6e03c72f4 100644
--- a/webapps/docs/web-socket-howto.xml
+++ b/webapps/docs/web-socket-howto.xml
@@ -63,13 +63,23 @@
    the timeout to use in milliseconds. For an infinite timeout, use
    <code>-1</code>.</p>
 
-<p>The session close timeout defaults to 30000 milliseconds (30 seconds). This
-   may be changed by setting the property
+<p>The time Tomcat waits for a peer to send a WebSocket session close message
+   after Tomcat has sent a close message to the peer defaults to 30000
+   milliseconds (30 seconds). This may be changed by setting the property
    <code>org.apache.tomcat.websocket.SESSION_CLOSE_TIMEOUT</code> in the user
    properties collection attached to the WebSocket session. The value assigned
    to this property should be a <code>Long</code> and represents the timeout to
    use in milliseconds. Values less than or equal to zero will be ignored.</p>
 
+<p>The write timeout Tomcat uses when writing a session close message when the
+   close is abnormal defaults to 50 milliseconds. This may be changed by 
setting
+   the property
+   <code>org.apache.tomcat.websocket.ABNORMAL_SESSION_CLOSE_SEND_TIMEOUT</code>
+   in the user properties collection attached to the WebSocket session. The
+   value assigned to this property should be a <code>Long</code> and represents
+   the timeout to use in milliseconds. Values less than or equal to zero will 
be
+   ignored.</p>
+
 <p>In addition to the <code>Session.setMaxIdleTimeout(long)</code> method which
    is part of the Java WebSocket API, Tomcat provides greater control of the
    timing out the session due to lack of activity. Setting the property


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to