Author: markt Date: Thu Jul 22 09:12:18 2010 New Revision: 966548 URL: http://svn.apache.org/viewvc?rev=966548&view=rev Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=49567 Better handling of calls to complete() from a separate thread. There are still a handful of TODOs around this which will follow shortly but I'm committing this since it passes the unit tests and the Servlet TCK (with BIO)
Modified: tomcat/trunk/java/org/apache/catalina/connector/CoyoteAdapter.java tomcat/trunk/java/org/apache/catalina/connector/Request.java tomcat/trunk/java/org/apache/catalina/core/AsyncContextImpl.java tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/catalina/connector/CoyoteAdapter.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/CoyoteAdapter.java?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/connector/CoyoteAdapter.java (original) +++ tomcat/trunk/java/org/apache/catalina/connector/CoyoteAdapter.java Thu Jul 22 09:12:18 2010 @@ -267,7 +267,7 @@ public class CoyoteAdapter implements Ad //configure settings for timed out asyncConImpl.setTimeoutState(); } - if (status==SocketStatus.ERROR || status==SocketStatus.STOP || status==SocketStatus.DISCONNECT) { + if (status==SocketStatus.ERROR || status==SocketStatus.DISCONNECT) { AsyncContextImpl asyncConImpl = (AsyncContextImpl)request.getAsyncContext(); //TODO SERVLET3 - async //configure settings for timed out Modified: tomcat/trunk/java/org/apache/catalina/connector/Request.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/Request.java?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/connector/Request.java (original) +++ tomcat/trunk/java/org/apache/catalina/connector/Request.java Thu Jul 22 09:12:18 2010 @@ -1582,7 +1582,6 @@ public class Request } return (asyncContext.getState()==AsyncContextImpl.AsyncState.DISPATCHING || - asyncContext.getState()==AsyncContextImpl.AsyncState.DISPATCHING_RUNNABLE || asyncContext.getState()==AsyncContextImpl.AsyncState.TIMING_OUT || asyncContext.getState()==AsyncContextImpl.AsyncState.STARTED || asyncContext.getState()==AsyncContextImpl.AsyncState.ERROR_DISPATCHING || Modified: tomcat/trunk/java/org/apache/catalina/core/AsyncContextImpl.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/AsyncContextImpl.java?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/AsyncContextImpl.java (original) +++ tomcat/trunk/java/org/apache/catalina/core/AsyncContextImpl.java Thu Jul 22 09:12:18 2010 @@ -48,8 +48,8 @@ import org.apache.juli.logging.LogFactor public class AsyncContextImpl implements AsyncContext { public static enum AsyncState { - NOT_STARTED, STARTED, DISPATCHING, DISPATCHING_RUNNABLE, DISPATCHED, - COMPLETING, COMPLETING_RUNNABLE, TIMING_OUT, ERROR_DISPATCHING + NOT_STARTED, STARTED, DISPATCHING, DISPATCHED, COMPLETING, TIMING_OUT, + ERROR_DISPATCHING } private static final Log log = LogFactory.getLog(AsyncContextImpl.class); @@ -87,9 +87,6 @@ public class AsyncContextImpl implements AtomicBoolean dispatched = new AtomicBoolean(false); request.getCoyoteRequest().action(ActionCode.ACTION_ASYNC_COMPLETE,dispatched); if (!dispatched.get()) doInternalComplete(false); - } else if (state.compareAndSet(AsyncState.DISPATCHING_RUNNABLE, - AsyncState.COMPLETING_RUNNABLE)) { - // do nothing } else { throw new IllegalStateException("Complete not allowed. Invalid state:"+state.get()); } @@ -182,33 +179,16 @@ public class AsyncContextImpl implements log.debug("AsyncContext Start Called["+state.get()+"; "+request.getRequestURI()+"?"+request.getQueryString()+"]", new DebugException()); } - if (state.compareAndSet(AsyncState.STARTED, AsyncState.DISPATCHING_RUNNABLE) || - state.compareAndSet(AsyncState.DISPATCHED, AsyncState.DISPATCHING_RUNNABLE)) { - // TODO SERVLET3 - async - final ServletContext sctx = getServletRequest().getServletContext(); - Runnable r = new Runnable() { - public void run() { - //TODO SERVLET3 - async - set context class loader when running the task. - try { - - run.run(); - }catch (Exception x) { - log.error("Unable to run async task.",x); - } - } - }; - this.dispatch = r; - AtomicBoolean dispatched = new AtomicBoolean(false); - request.getCoyoteRequest().action(ActionCode.ACTION_ASYNC_DISPATCH, dispatched ); - if (!dispatched.get()) { - try { - doInternalDispatch(); - }catch (ServletException sx) { - throw new RuntimeException(sx); - }catch (IOException ix) { - throw new RuntimeException(ix); - } - } + if (state.get() == AsyncState.STARTED) { + // TODO SERVLET3 - async - set context class loader when running the + // task. + // final ServletContext sctx = getServletRequest().getServletContext(); + // TODO - Use a container thread without creating a memory leak + // Execute the runnable using a container thread from the + // Connector's thread pool + // request.getConnector().getProtocolHandler().getExecutor().execute(run); + Thread t = new Thread(run); + t.start(); } else { throw new IllegalStateException("Dispatch not allowed. Invalid state:"+state.get()); } @@ -258,8 +238,7 @@ public class AsyncContextImpl implements public boolean isStarted() { return (state.get() == AsyncState.STARTED || - state.get() == AsyncState.DISPATCHING || - state.get() == AsyncState.DISPATCHING_RUNNABLE); + state.get() == AsyncState.DISPATCHING); } public void setStarted(Context context) { @@ -304,10 +283,16 @@ public class AsyncContextImpl implements listener.fireOnTimeout(event); listenerInvoked = true; } - if (!listenerInvoked) { - ((HttpServletResponse)servletResponse).setStatus(500); + if (listenerInvoked) { + // Listener should have called complete + if (state.get() != AsyncState.NOT_STARTED) { + ((HttpServletResponse)servletResponse).setStatus(500); + doInternalComplete(true); + } + } else { + // No listeners, container calls complete + doInternalComplete(false); } - doInternalComplete(true); } else if (this.state.compareAndSet(AsyncState.ERROR_DISPATCHING, AsyncState.COMPLETING)) { log.debug("ON ERROR!"); boolean listenerInvoked = false; @@ -339,27 +324,6 @@ public class AsyncContextImpl implements dispatch = null; } } - } else if (this.state.get() == AsyncState.DISPATCHING_RUNNABLE) { - if (this.dispatch!=null) { - try { - dispatch.run(); - } catch (RuntimeException x) { - doInternalComplete(true); - if (x.getCause() instanceof ServletException) throw (ServletException)x.getCause(); - if (x.getCause() instanceof IOException) throw (IOException)x.getCause(); - else throw new ServletException(x); - } finally { - dispatch = null; - } - if (this.state.compareAndSet(AsyncState.COMPLETING_RUNNABLE, - AsyncState.COMPLETING)) { - doInternalComplete(false); - } else if (this.state.get() == AsyncState.DISPATCHING_RUNNABLE) { - doInternalComplete(true); - throw new IllegalStateException( - "Failed to call dispatch() or complete() after start()"); - } - } } else if (this.state.get()==AsyncState.COMPLETING) { doInternalComplete(false); } else { Modified: tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java Thu Jul 22 09:12:18 2010 @@ -349,8 +349,8 @@ public class Http11Processor extends Abs } else if (async) { return SocketState.LONG; } else { + recycle(); if (!keepAlive) { - recycle(); return SocketState.CLOSED; } else { return SocketState.OPEN; Modified: tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java (original) +++ tomcat/trunk/test/org/apache/catalina/core/TestAsyncContextImpl.java Thu Jul 22 09:12:18 2010 @@ -19,6 +19,7 @@ package org.apache.catalina.core; import java.io.IOException; +import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -28,6 +29,7 @@ import org.apache.catalina.Context; import org.apache.catalina.Wrapper; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; public class TestAsyncContextImpl extends TomcatBaseTest { @@ -48,11 +50,90 @@ public class TestAsyncContextImpl extend tomcat.start(); // Call the servlet once - getUrl("http://localhost:" + getPort() + "/"); + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + assertEquals("OK", bc.toString()); + + + assertEquals("1false2true3true4true5false", servlet.getResult()); + } + + public void testBug49567() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("/", System.getProperty("java.io.tmpdir")); + + Bug49567Servlet servlet = new Bug49567Servlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMapping("/", "servlet"); + + tomcat.start(); + + // Call the servlet once + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + assertEquals("OK", bc.toString()); assertEquals("1false2true3true4true5false", servlet.getResult()); } + public void testAsyncStartNoComplete() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Minimise pauses during test + tomcat.getConnector().setAttribute( + "connectionTimeout", Integer.valueOf(3000)); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("/", System.getProperty("java.io.tmpdir")); + + AsyncStartNoCompleteServlet servlet = + new AsyncStartNoCompleteServlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMapping("/", "servlet"); + + tomcat.start(); + + // Call the servlet the first time + ByteChunk bc1 = getUrl("http://localhost:" + getPort() + + "/?echo=run1"); + assertEquals("OK-run1", bc1.toString()); + + // Call the servlet the second time with a request parameter + ByteChunk bc2 = getUrl("http://localhost:" + getPort() + + "/?echo=run2"); + assertEquals("OK-run2", bc2.toString()); + } + + public void testAsyncStartWithComplete() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("/", System.getProperty("java.io.tmpdir")); + + AsyncStartWithCompleteServlet servlet = + new AsyncStartWithCompleteServlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMapping("/", "servlet"); + + tomcat.start(); + + // Call the servlet once + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + assertEquals("OK", bc.toString()); + } + private static class Bug49528Servlet extends HttpServlet { private static final long serialVersionUID = 1L; @@ -83,12 +164,15 @@ public class TestAsyncContextImpl extend Thread.sleep(1000); result.append('4'); result.append(req.isAsyncStarted()); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); req.getAsyncContext().complete(); result.append('5'); result.append(req.isAsyncStarted()); } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + result.append(e); + } catch (IOException e) { + result.append(e); } } }); @@ -97,4 +181,96 @@ public class TestAsyncContextImpl extend req.getMethod(); } } + + private static class Bug49567Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private StringBuilder result = new StringBuilder(); + + public String getResult() { + return result.toString(); + } + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + result.append('1'); + result.append(req.isAsyncStarted()); + req.startAsync(); + result.append('2'); + result.append(req.isAsyncStarted()); + + req.getAsyncContext().start(new Runnable() { + @Override + public void run() { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + result.append('3'); + result.append(req.isAsyncStarted()); + Thread.sleep(1000); + result.append('4'); + result.append(req.isAsyncStarted()); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + req.getAsyncContext().complete(); + result.append('5'); + result.append(req.isAsyncStarted()); + } catch (InterruptedException e) { + result.append(e); + } catch (IOException e) { + result.append(e); + } + } + }); + t.start(); + } + }); + // Pointless method call so there is somewhere to put a break point + // when debugging + req.getMethod(); + } + } + + private static class AsyncStartNoCompleteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + String echo = req.getParameter("echo"); + AsyncContext actxt = req.startAsync(); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + if (echo != null) { + resp.getWriter().print("-" + echo); + } + // Speed up the test by reducing the timeout + actxt.setTimeout(1000); + } + } + + private static class AsyncStartWithCompleteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext actxt = req.startAsync(); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + actxt.complete(); + } + } + } Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=966548&r1=966547&r2=966548&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Thu Jul 22 09:12:18 2010 @@ -122,11 +122,11 @@ rather than an empty string. (markt) </fix> <fix> - <bug>49528</bug>: Ensure AsyncContext.isAsyncStarted() returns the - correct value after AsyncContext.start(). Tomcat implements this - using an internal dispatch that requires slightly different treatment - from a standard dispatch to ensure the correct value is returned. - (markt) + <bug>49528</bug>, <bug>49567</bug>: Ensure that + <code>AsyncContext.isAsyncStarted()</code> returns the correct value + after <code>AsyncContext.start()</code> and that if + <code>AsyncContext.complete()</code> is called on a separate thread that + it is handled correctly. (markt) </fix> <fix> <bug>49530</bug>: Contexts and Servlets not stopped when Tomcat is shut --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org