You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2021/02/25 13:38:26 UTC
[tomcat] 02/04: Ensure AsyncListener.onError() is triggered for a
non-blocking IO error
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 4cb74d1e52f2cf0503a67889eecb1a4dded17f43
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Wed Feb 24 17:11:16 2021 +0000
Ensure AsyncListener.onError() is triggered for a non-blocking IO error
If the non-blocking I/O error occurred in onWritePossible() or
onDataAvailable() (rather than when buffered data was being written in
the background) the error was not propogated to the AsyncListener.
---
.../apache/catalina/connector/CoyoteAdapter.java | 2 +
.../catalina/nonblocking/TestNonBlockingAPI.java | 169 +++++++++++++++++++--
webapps/docs/changelog.xml | 5 +
3 files changed, 163 insertions(+), 13 deletions(-)
diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 2288451..19bb1d3 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -192,6 +192,7 @@ public class CoyoteAdapter implements Adapter {
// https://bz.apache.org/bugzilla/show_bug.cgi?id=65001
res.action(ActionCode.CLOSE_NOW, t);
writeListener.onError(t);
+ asyncConImpl.setErrorState(t, true);
} finally {
request.getContext().unbind(false, oldCL);
}
@@ -219,6 +220,7 @@ public class CoyoteAdapter implements Adapter {
// https://bz.apache.org/bugzilla/show_bug.cgi?id=65001
res.action(ActionCode.CLOSE_NOW, t);
readListener.onError(t);
+ asyncConImpl.setErrorState(t, true);
} finally {
request.getContext().unbind(false, oldCL);
}
diff --git a/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
index 6a6d478..c33539b 100644
--- a/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
+++ b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java
@@ -317,7 +317,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
@Test
- public void testNonBlockingWriteError() throws Exception {
+ public void testNonBlockingWriteError01() throws Exception {
Tomcat tomcat = getTomcatInstance();
// No file system docBase required
@@ -387,8 +387,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
// Listeners are invoked and access valve entries created on a different
// thread so give that thread a chance to complete its work.
int count = 0;
- while (count < 100 &&
- !(servlet.wlistener.onErrorInvoked || servlet.rlistener.onErrorInvoked)) {
+ while (count < 100 && !servlet.wlistener.onErrorInvoked) {
Thread.sleep(100);
count ++;
}
@@ -398,8 +397,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
count ++;
}
- Assert.assertTrue("Error listener should have been invoked.",
- servlet.wlistener.onErrorInvoked || servlet.rlistener.onErrorInvoked);
+ Assert.assertTrue("Error listener should have been invoked.", servlet.wlistener.onErrorInvoked);
// TODO Figure out why non-blocking writes with the NIO connector appear
// to be slower on Linux
@@ -550,7 +548,6 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
private static final long serialVersionUID = 1L;
private final boolean unlimited;
public transient volatile TestWriteListener wlistener;
- public transient volatile TestReadListener rlistener;
public NBWriteServlet() {
this(false);
@@ -590,9 +587,6 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
}
});
// step 2 - notify on read
- ServletInputStream in = req.getInputStream();
- rlistener = new TestReadListener(actx, true, false);
- in.setReadListener(rlistener);
ServletOutputStream out = resp.getOutputStream();
resp.setBufferSize(200 * 1024);
wlistener = new TestWriteListener(actx, unlimited);
@@ -625,7 +619,6 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
protected final boolean usingNonBlockingWrite;
protected final boolean ignoreIsReady;
protected final StringBuilder body = new StringBuilder();
- public volatile boolean onErrorInvoked = false;
public TestReadListener(AsyncContext ctx,
@@ -678,7 +671,6 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
public void onError(Throwable throwable) {
log.info("ReadListener.onError totalData=" + body.toString().length());
throwable.printStackTrace();
- onErrorInvoked = true;
}
}
@@ -1181,7 +1173,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
tomcat.start();
- PostClient client = new PostClient();
+ ResponseOKClient client = new ResponseOKClient();
client.setPort(getPort());
client.setRequest(request);
client.connect();
@@ -1198,7 +1190,7 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
}
- private static final class PostClient extends SimpleHttpClient {
+ private static final class ResponseOKClient extends SimpleHttpClient {
@Override
public boolean isResponseBodyOK() {
@@ -1319,4 +1311,155 @@ public class TestNonBlockingAPI extends TomcatBaseTest {
ac.complete();
}
}
+
+
+ /*
+ * Tests client disconnect in the following scenario:
+ * - async with non-blocking IO
+ * - response has been committed
+ * - no data in buffers
+ * - client disconnects
+ * - server attempts a write
+ */
+ @Test
+ public void testNonBlockingWriteError02() throws Exception {
+ CountDownLatch responseCommitLatch = new CountDownLatch(1);
+ CountDownLatch clientCloseLatch = new CountDownLatch(1);
+ CountDownLatch asyncCompleteLatch = new CountDownLatch(1);
+
+ // Setup Tomcat instance
+ Tomcat tomcat = getTomcatInstance();
+
+ // No file system docBase required
+ Context ctx = tomcat.addContext("", null);
+
+ NBWriteServlet02 writeServlet = new NBWriteServlet02(responseCommitLatch, clientCloseLatch, asyncCompleteLatch);
+ Wrapper wrapper = Tomcat.addServlet(ctx, "writeServlet", writeServlet);
+ wrapper.setAsyncSupported(true);
+ ctx.addServletMappingDecoded("/*", "writeServlet");
+
+ tomcat.start();
+
+ ResponseOKClient client = new ResponseOKClient();
+ client.setPort(getPort());
+ client.setRequest(new String[] {
+ "GET / HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF
+ });
+ client.connect();
+ client.sendRequest();
+
+ responseCommitLatch.await();
+
+ client.disconnect();
+ clientCloseLatch.countDown();
+
+ Assert.assertTrue("Failed to complete async processing", asyncCompleteLatch.await(10, TimeUnit.SECONDS));
+ }
+
+
+ private static class NBWriteServlet02 extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private final CountDownLatch responseCommitLatch;
+ private final CountDownLatch clientCloseLatch;
+ private final CountDownLatch asyncCompleteLatch;
+
+ public NBWriteServlet02(CountDownLatch responseCommitLatch, CountDownLatch clientCloseLatch,
+ CountDownLatch asyncCompleteLatch) {
+ this.responseCommitLatch = responseCommitLatch;
+ this.clientCloseLatch = clientCloseLatch;
+ this.asyncCompleteLatch = asyncCompleteLatch;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding("UTF-8");
+
+ AsyncContext ac = req.startAsync();
+ ac.addListener(new TestAsyncListener02(asyncCompleteLatch));
+ ac.setTimeout(5000);
+
+ WriteListener writeListener = new TestWriteListener02(ac, responseCommitLatch, clientCloseLatch);
+ resp.getOutputStream().setWriteListener(writeListener);
+ }
+ }
+
+
+ private static class TestAsyncListener02 implements AsyncListener {
+
+ private final CountDownLatch asyncCompleteLatch;
+
+ public TestAsyncListener02(CountDownLatch asyncCompleteLatch) {
+ this.asyncCompleteLatch = asyncCompleteLatch;
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException {
+ asyncCompleteLatch.countDown();
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException {
+ // NO-OP
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException {
+ // NO-OP
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException {
+ // NO-OP
+ }
+
+ }
+
+ private static class TestWriteListener02 implements WriteListener {
+
+ private final AsyncContext ac;
+ private final CountDownLatch responseCommitLatch;
+ private final CountDownLatch clientCloseLatch;
+ private volatile int stage = 0;
+
+ public TestWriteListener02(AsyncContext ac, CountDownLatch responseCommitLatch,
+ CountDownLatch clientCloseLatch) {
+ this.ac = ac;
+ this.responseCommitLatch = responseCommitLatch;
+ this.clientCloseLatch = clientCloseLatch;
+ }
+
+ @Override
+ public void onWritePossible() throws IOException {
+ ServletOutputStream sos = ac.getResponse().getOutputStream();
+ do {
+ if (stage == 0) {
+ // Commit the response
+ ac.getResponse().flushBuffer();
+ responseCommitLatch.countDown();
+ stage++;
+ } else if (stage == 1) {
+ // Wait for the client to drop the connection
+ try {
+ clientCloseLatch.await();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ sos.print("TEST");
+ stage++;
+ } else if (stage == 2) {
+ sos.flush();
+ }
+ } while (sos.isReady());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ // NO-OP
+ }
+ }
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index a46de2c..dba2eb7 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -120,6 +120,11 @@
<code>getParallelAnnotationScanning</code> for consistency and ease
of use in JMX descriptors. (remm)
</fix>
+ <fix>
+ Ensure that the <code>AsyncListener.onError()</code> event is triggered
+ when a I/O error occurs during non-blocking I/O. There were some cases
+ discovered where this was not happening. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org