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


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 9b205ce9d9 Fix BZ 69068 - trigger read timeouts for async reads on 
HTTP/2
9b205ce9d9 is described below

commit 9b205ce9d968972fa9f6dd3848307d662dcc18c8
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Sun Jun 2 15:22:54 2024 +0100

    Fix BZ 69068 - trigger read timeouts for async reads on HTTP/2
    
    Test case provided by hypnoce
---
 .../apache/coyote/http2/LocalStrings.properties    |   1 +
 java/org/apache/coyote/http2/Stream.java           |  23 ++++
 java/org/apache/coyote/http2/StreamProcessor.java  |  20 +++
 test/org/apache/coyote/http2/Http2TestBase.java    |   2 +-
 .../apache/coyote/http2/TestAsyncReadListener.java | 145 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   8 ++
 6 files changed, 198 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/coyote/http2/LocalStrings.properties 
b/java/org/apache/coyote/http2/LocalStrings.properties
index 137e3346db..891ba7ad81 100644
--- a/java/org/apache/coyote/http2/LocalStrings.properties
+++ b/java/org/apache/coyote/http2/LocalStrings.properties
@@ -121,6 +121,7 @@ streamProcessor.error.connection=Connection [{0}], Stream 
[{1}], An error occurr
 streamProcessor.error.stream=Connection [{0}], Stream [{1}], An error occurred 
during processing that was fatal to the stream
 streamProcessor.flushBufferedWrite.entry=Connection [{0}], Stream [{1}], 
Flushing buffered writes
 streamProcessor.service.error=Error during request processing
+streamProcessor.streamReadTimeout=Stream read timeout
 
 streamStateMachine.debug.change=Connection [{0}], Stream [{1}], State changed 
from [{2}] to [{3}]
 streamStateMachine.invalidFrame=Connection [{0}], Stream [{1}], State [{2}], 
Frame type [{3}]
diff --git a/java/org/apache/coyote/http2/Stream.java 
b/java/org/apache/coyote/http2/Stream.java
index a357d34cf7..9d13db7c72 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -1160,6 +1160,8 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
         abstract boolean isRequestBodyFullyRead();
 
         abstract void insertReplayedBody(ByteChunk body);
+
+        protected abstract boolean timeoutRead(long now);
     }
 
 
@@ -1186,6 +1188,8 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
         // 'write mode'.
         private volatile ByteBuffer inBuffer;
         private volatile boolean readInterest;
+        // If readInterest is true, data must be available to read no later 
than this time.
+        private volatile long readTimeoutExpiry;
         private volatile boolean closed;
         private boolean resetReceived;
 
@@ -1285,6 +1289,12 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
 
                 if (!isRequestBodyFullyRead()) {
                     readInterest = true;
+                    long readTimeout = 
handler.getProtocol().getStreamReadTimeout();
+                    if (readTimeout > 0) {
+                        readTimeoutExpiry = System.currentTimeMillis() + 
handler.getProtocol().getStreamReadTimeout();
+                    } else {
+                        readTimeoutExpiry = Long.MAX_VALUE;
+                    }
                 }
 
                 return false;
@@ -1436,6 +1446,12 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
                 }
             }
         }
+
+
+        @Override
+        protected boolean timeoutRead(long now) {
+            return readInterest && now > readTimeoutExpiry;
+        }
     }
 
 
@@ -1497,5 +1513,12 @@ class Stream extends AbstractNonZeroStream implements 
HeaderEmitter {
         void insertReplayedBody(ByteChunk body) {
             // NO-OP
         }
+
+
+        @Override
+        protected boolean timeoutRead(long now) {
+            // Reading from a saved request. Will never time out.
+            return false;
+        }
     }
 }
diff --git a/java/org/apache/coyote/http2/StreamProcessor.java 
b/java/org/apache/coyote/http2/StreamProcessor.java
index 0e99606f57..872cc75687 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -18,6 +18,7 @@ package org.apache.coyote.http2;
 
 import java.io.File;
 import java.io.IOException;
+import java.net.SocketTimeoutException;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -25,6 +26,7 @@ import java.util.Set;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import javax.servlet.RequestDispatcher;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.coyote.AbstractProcessor;
@@ -554,4 +556,22 @@ class StreamProcessor extends AbstractProcessor {
     protected final SocketState dispatchEndRequest() throws IOException {
         return SocketState.CLOSED;
     }
+
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * First checks for a stream read timeout and processes it if detected. If 
no stream read timeout is detected then
+     * the superclass is called to check for an asynchronous processing 
timeout.
+     */
+    @Override
+    public void timeoutAsync(long now) {
+        if (stream.getInputBuffer().timeoutRead(now)) {
+            
stream.getCoyoteRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION,
+                    new 
SocketTimeoutException(sm.getString("streamProcessor.streamReadTimeout")));
+            processSocketEvent(SocketEvent.ERROR, true);
+        } else {
+            super.timeoutAsync(now);
+        }
+    }
 }
diff --git a/test/org/apache/coyote/http2/Http2TestBase.java 
b/test/org/apache/coyote/http2/Http2TestBase.java
index 82dc91726f..42def01e9b 100644
--- a/test/org/apache/coyote/http2/Http2TestBase.java
+++ b/test/org/apache/coyote/http2/Http2TestBase.java
@@ -111,7 +111,7 @@ public abstract class Http2TestBase extends TomcatBaseTest {
     protected static final String TRAILER_HEADER_VALUE = "test";
 
     // Client
-    private Socket s;
+    protected Socket s;
     protected HpackEncoder hpackEncoder;
     protected Input input;
     protected TestOutput output;
diff --git a/test/org/apache/coyote/http2/TestAsyncReadListener.java 
b/test/org/apache/coyote/http2/TestAsyncReadListener.java
new file mode 100644
index 0000000000..28a32f825e
--- /dev/null
+++ b/test/org/apache/coyote/http2/TestAsyncReadListener.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.coyote.http2;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.Tomcat;
+
+public class TestAsyncReadListener extends Http2TestBase {
+
+    private AsyncServlet asyncServlet;
+
+    @Before
+    public void before() {
+        asyncServlet = new AsyncServlet();
+    }
+
+    @Test
+    public void testEmptyWindow() throws Exception {
+        http2Connect();
+
+        byte[] headersFrameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+        byte[] dataFrameHeader = new byte[9];
+        ByteBuffer dataPayload = ByteBuffer.allocate(256);
+        byte[] trailerFrameHeader = new byte[9];
+        ByteBuffer trailerPayload = ByteBuffer.allocate(256);
+
+
+        buildPostRequest(headersFrameHeader, headersPayload, false, null, -1, 
"/async", dataFrameHeader, dataPayload,
+            null, trailerFrameHeader, trailerPayload, 3);
+
+        synchronized (asyncServlet) {
+            // Write the headers
+            writeFrame(headersFrameHeader, headersPayload);
+            asyncServlet.wait(4000);
+            s.close();
+            asyncServlet.wait(4000);
+        }
+        Assert.assertNotNull(asyncServlet.t);
+    }
+
+    @Override
+    protected void configureAndStartWebApplication() throws LifecycleException 
{
+        Tomcat tomcat = getTomcatInstance();
+
+        Context ctxt = getProgrammaticRootContext();
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+        Wrapper w = Tomcat.addServlet(ctxt, "async", asyncServlet);
+        w.setAsyncSupported(true);
+        ctxt.addServletMappingDecoded("/async", "async");
+        tomcat.start();
+    }
+
+    public static class AsyncServlet extends HttpServlet {
+        private static final long serialVersionUID = 1L;
+        private volatile Throwable t;
+        @Override
+        protected void doPost(HttpServletRequest req, HttpServletResponse 
resp) throws ServletException, IOException {
+            final AsyncContext asyncContext = req.startAsync();
+            asyncContext.getRequest().getInputStream().setReadListener(new 
ReadListener() {
+                @Override
+                public void onDataAvailable() throws IOException {
+                    System.out.println("RL-onDataAvailable");
+                    synchronized (AsyncServlet.this) {
+                        AsyncServlet.this.notifyAll();
+                    }
+                }
+
+                @Override
+                public void onAllDataRead() throws IOException {
+                    System.out.println("RL-onAllDataRead");
+                }
+
+                @Override
+                public void onError(Throwable throwable) {
+                    System.out.println("RL-onError ");
+                    System.out.println(throwable);
+                    synchronized (AsyncServlet.this) {
+                        t = throwable;
+                        AsyncServlet.this.notifyAll();
+                        asyncContext.complete();
+                    }
+                }
+            });
+            System.out.println("setReadListener");
+            asyncContext.addListener(new AsyncListener() {
+
+                @Override
+                public void onComplete(AsyncEvent event) throws IOException {
+                    System.out.println("AL-onComplete");
+                }
+
+                @Override
+                public void onTimeout(AsyncEvent event) throws IOException {
+                    System.out.println("AL-onTimeout");
+                }
+
+                @Override
+                public void onError(AsyncEvent event) throws IOException {
+                    System.out.println("AL-onError");
+                }
+
+                @Override
+                public void onStartAsync(AsyncEvent event) throws IOException {
+                    System.out.println("AL-onStartAsync");
+                }
+            });
+            System.out.println("setAsyncListener");
+        }
+    }
+}
\ No newline at end of file
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index fe80b25520..f5d9127bc1 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -124,6 +124,14 @@
       </update>
     </changelog>
   </subsection>
+  <subsection name="Coyote">
+    <changelog>
+      <fix>
+        <bug>69068</bug>: Ensure read timouts are triggered for asynchronous,
+        non-blocking reads when using HTTP/2. (markt)
+      </fix>
+    </changelog>
+  </subsection>
   <subsection name="Other">
     <changelog>
       <update>


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

Reply via email to