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

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

commit 0b52f13b35b0d9c0454f149aa863ef982648bab0
Author: Kai Burjack <[email protected]>
AuthorDate: Tue Feb 10 21:49:59 2026 +0100

    Add drainTimeout property to Http2Protocol/Http2UpgradeHandler (#917)
    
    This is to delay sending the second GOAWAY (with last seen stream id)
    after sending the first GOAWAY (with max stream id), in order to
    mitigate a race between the client already having buffered frames
    for new streams after the server has sent the first graceful GOAWAY
    frame and the second GOAWAY.
    
    Once the second GOAWAY frame reaches the client, its own HTTP/2 client
    implementation may then itself signal a stream reset (rejected stream)
    error condition to the client application or might send the new
    forbidden stream frames to the server, where it then would be rejected.
    
    Therefore, we implement the same behaviour as provided by the Envoy
    proxy's HTTP Connection Manager drain_timeout: Delay sending the second
    final GOAWAY after sending the first graceful GOAWAY to give the client
    enough time to react to the first GOAWAY and stop sending new streams
    and close the connection on its side first.
---
 java/org/apache/coyote/http2/Http2Protocol.java    | 23 ++++++++++++++++++++++
 .../apache/coyote/http2/Http2UpgradeHandler.java   |  6 +++++-
 2 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/coyote/http2/Http2Protocol.java 
b/java/org/apache/coyote/http2/Http2Protocol.java
index b8a7d6fde6..efc961ffce 100644
--- a/java/org/apache/coyote/http2/Http2Protocol.java
+++ b/java/org/apache/coyote/http2/Http2Protocol.java
@@ -112,6 +112,19 @@ public class Http2Protocol implements UpgradeProtocol {
     private boolean discardRequestsAndResponses = false;
     private final SynchronizedStack<Request> recycledRequestsAndResponses = 
new SynchronizedStack<>();
 
+    /*
+     * Additional time in nanoseconds between sending the first graceful 
GOAWAY (max stream id) and the final GOAWAY
+     * (last seen stream id). During this time the server will continue to 
process new streams on the connection.
+     * This is to mitigate the race of client-buffered/sent packets for new 
streams and the final GOAWAY (with last
+     * seen stream id). By default, Tomcat uses the last computed RTT for this 
interval, but the RTT might have
+     * fluctuated due to network or server load conditions, or the client 
(e.g. nghttp2) might have already buffered
+     * frames for opening new streams on a connection.
+     *
+     * The name "drainTimeout" is taken from Envoy proxy's identical HTTP 
Connection Manager property and means exactly
+     * the same.
+     */
+    private long drainTimeout;
+
     @Override
     public String getHttpUpgradeName(boolean isSSLEnabled) {
         if (isSSLEnabled) {
@@ -409,6 +422,16 @@ public class Http2Protocol implements UpgradeProtocol {
     }
 
 
+    public long getDrainTimeout() {
+        return drainTimeout;
+    }
+
+
+    public void setDrainTimeout(long drainTimeout) {
+        this.drainTimeout = drainTimeout;
+    }
+
+
     Request popRequestAndResponse() {
         Request requestAndResponse = null;
         if (!discardRequestsAndResponses) {
diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java 
b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
index ba7cc175bc..101bf0c4ac 100644
--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
@@ -139,6 +139,8 @@ class Http2UpgradeHandler extends AbstractStream implements 
InternalHttpUpgradeH
     private volatile int lastNonFinalDataPayload;
     private volatile int lastWindowUpdate;
 
+    // Time between the "graceful" GOAWAY (max stream id) and the final GOAWAY 
(last seen stream id)
+    private long drainTimeout = 0;
 
     Http2UpgradeHandler(Http2Protocol protocol, Adapter adapter, Request 
coyoteRequest,
             SocketWrapperBase<?> socketWrapper) {
@@ -172,6 +174,8 @@ class Http2UpgradeHandler extends AbstractStream implements 
InternalHttpUpgradeH
 
         pingManager.initiateDisabled = protocol.getInitiatePingDisabled();
 
+        drainTimeout = protocol.getDrainTimeout();
+
         // Initial HTTP request becomes stream 1.
         if (coyoteRequest != null) {
             if (log.isTraceEnabled()) {
@@ -542,7 +546,7 @@ class Http2UpgradeHandler extends AbstractStream implements 
InternalHttpUpgradeH
 
     void checkPauseState() throws IOException {
         if (connectionState.get() == ConnectionState.PAUSING) {
-            if (pausedNanoTime + pingManager.getRoundTripTimeNano() < 
System.nanoTime()) {
+            if (pausedNanoTime + pingManager.getRoundTripTimeNano() + 
drainTimeout < System.nanoTime()) {
                 connectionState.compareAndSet(ConnectionState.PAUSING, 
ConnectionState.PAUSED);
                 writeGoAwayFrame(maxProcessedStreamId, 
Http2Error.NO_ERROR.getCode(), null);
             }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to