diff --git a/jetty-documentation/src/main/asciidoc/reference/upgrading/upgrading-9.3-to-9.4.adoc b/jetty-documentation/src/main/asciidoc/reference/upgrading/upgrading-9.3-to-9.4.adoc index f0b73c0e280..d113334d24c 100644 --- a/jetty-documentation/src/main/asciidoc/reference/upgrading/upgrading-9.3-to-9.4.adoc +++ b/jetty-documentation/src/main/asciidoc/reference/upgrading/upgrading-9.3-to-9.4.adoc @@ -32,7 +32,7 @@ You can safely replace Jetty 9.3's `jetty.sh` with 9.4's. | `logging` | `console-capture` | `infinispan` | `session-store-infinispan-embedded` or `session-store-infinispan-remote` | `jdbc-sessions` | `session-store-jdbc` -| `gcloud-memcached-sessions`, `gcloud-session-idmgr` and `gcloud-sessions` | `gcloud`, `gcloud-datastore` and `session-store-gcloud` +| `gcloud-memcached-sessions`, `gcloud-session-idmgr` and `gcloud-sessions` | `session-store-gcloud` and `session-store-cache` | `nosql` | `session-store-mongo` |=== @@ -88,8 +88,92 @@ For information on logging modules in the Jetty 9.4 architecture please see the //TODO - More info. -Session management received a significant overhaul in Jetty 9.4. Whereas in prior versions of Jetty uses needed to implement individual instances of both `SessionIdManager` and `SessionManager`, now one instance of both handles sessions for the server. +Session management received a significant overhaul in Jetty 9.4. +Session functionality has been refactored to promote code-reuse, easier configuration and easier customization. +Whereas previously users needed to edit xml configuration files, in Jetty 9.4 all session behaviour is controlled by properties that are exposed by the various session modules. +Users now configure session management by selecting a composition of session modules. + +====== Change Overview + +SessionIdManager:: Previously there was a different class of SessionIdManager - with different configuration options - depending upon which type of clustering technology chosen. +In Jetty 9.4, there is only one type, the link:{JDURL}/org/eclipse/jetty/server/session/DefaultSessionIdManager.html[org.eclipse.jetty.server.session.DfeaultSessionIdManager]. + +SessionManager:: Previously, there was a different class of SessionManager depending upon which the type of clustering technology chosen. +In Jetty 9.4 we have removed the SessionManager class and split its functionality into different, more easily extensible and composable classes: +General setters::: +All of the common setup of sessions such as the maxInactiveInterval and session cookie-related configuration has been moved to the link:{JDURL}/org/eclipse/jetty/server/session/SessionHandler.html[org.eclipse.jetty.server.session.SessionHandler] +[cols="1,1", options="header"] +|=== +| 9.3 SessionManager | 9.4 SessionHandler +| setMaxInactiveInterval(sec) | setMaxInactiveInterval(sec) +| setSessionCookie(String) | setSessionCookie(String) +| setRefreshCookieAge(sec) | setRefreshCookieAge(sec) +| setSecureRequestOnly(boolean) | setSecureRequestOnly(boolean +| setSessionIdPathParameterName(String) | setSessionIdPathParameterName(String) +| setSessionTrackingModes(Set) | setSessionTrackingModes(Set) +| setHttpOnly(boolean) | setHttpOnly(boolean) +| setUsingCookies(boolean) | setUsingCookies(boolean) +| setCheckingRemoteSessionIdEncoding(boolean) | setCheckingRemoteSessionIdEncoding(boolean) +|=== + +Persistence::: +In Jetty 9.3 SessionManagers (and sometimes SessionIdManagers) implemented the persistence mechanism. +In Jetty 9.4 we have moved this functionality into the link:{JDURL}/org/eclipse/jetty/server/session/SessionDataStore.html[org.eclipse.jetty.server.session.SessionDataStore]. + +Session cache::: +In Jetty 9.3 the SessionManager held a map of session objects in memory. +In Jetty 9.4 this has been moved into the new link:{JDURL}/org/eclipse/jetty/server/session/SessionCache.html[org.eclipse.jetty.server.session.SessionCache] interface. -As part of these changes, modules for individual technologies were re-named to make configuration more transparent. For more information, please refer to the documentation on link:#jetty-sessions-architecture[Jetty Session Architecture.] + +====== Default + +As with earlier versions of jetty, if you do not explicitly configure any session modules, the default session infrastructure will be enabled. +In previous versions of jetty this was referred to as "hash" session management. +The new default provides similar features to the old hash session management: + * a session scavenger thread that runs every 10mins and removes expired sessions + * a session id manager that generates unique session ids and handles session id sharing during context forwarding + * an in-memory cache of session objects. +Requests for the same session in the same context share the same session object. +Session objects remain in the cache until they expire or are explicitly invalidated. + +If you wish to configure the default setup further, enable the `session-cache-default` module. + + +====== Filesystem + +In earlier versions of jetty, persisting sessions to the local filesystem was an option of the "hash" session manager. +In jetty-9.4 this has been refactored to its own configurable module `session-store-file`. + + +====== JDBC + +As with earlier versions of jetty, sessions may be persisted to a relational database. +Enable the `session-store-jdbc` module. + + +====== NoSQL + +As with earlier versions of jetty, sessions may be persisted to a document database. +Jetty supports the Mongo document database. +Enable the `session-store-mongo` module. + + +====== Infinispan + +As with earlier versions of jetty, sessions may be clustered via Infinispan to either an in-process or remote infinispan instance. +Enable the `session-store-infinispan` module. + + +====== GCloud Datastore + +As with earlier versions of jetty, sessions may be persisted to Google's GCloud Datastore. +Enable the `session-store-gcloud` module. + + +====== GCloud Datastore with Memcached + +As with earlier versions of jetty, sessions can be both persisted to Google's GCloud Datastore, and cached into Memcached for faster access. +Enable the `session-store-gcloud` and `session-store-cache` modules. + diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java index 65e1712d841..9bb82f80941 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpTransportOverFCGI.java @@ -30,9 +30,12 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; public class HttpTransportOverFCGI implements HttpTransport { + private static final Logger LOG = Log.getLogger(HttpTransportOverFCGI.class); private final ServerGenerator generator; private final Flusher flusher; private final int request; @@ -97,6 +100,8 @@ public class HttpTransportOverFCGI implements HttpTransport private void commit(MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("commit {} {} l={}",this,info,lastContent); boolean shutdown = this.shutdown = info.getFields().contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); if (head) @@ -137,7 +142,10 @@ public class HttpTransportOverFCGI implements HttpTransport @Override public void abort(Throwable failure) { + if (LOG.isDebugEnabled()) + LOG.debug("abort {} {}",this,failure); aborted = true; + flusher.shutdown(); } @Override diff --git a/jetty-fcgi/fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java b/jetty-fcgi/fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java index 5bf58b4ef9e..ec650db38ee 100644 --- a/jetty-fcgi/fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java +++ b/jetty-fcgi/fcgi-server/src/test/java/org/eclipse/jetty/fcgi/server/HttpClientTest.java @@ -608,7 +608,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest { client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .timeout(5, TimeUnit.SECONDS) + .timeout(60, TimeUnit.SECONDS) .send(); Assert.fail(); } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index 3415c883d71..f25f0da7b89 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -931,9 +931,10 @@ public class ProxyServletTest Assert.assertArrayEquals(content, response.getContent()); } - @Test(expected = TimeoutException.class) + @Test public void testWrongContentLength() throws Exception { + startServer(new HttpServlet() { @Override @@ -948,11 +949,17 @@ public class ProxyServletTest startProxy(); startClient(); - client.newRequest("localhost", serverConnector.getLocalPort()) - .timeout(1, TimeUnit.SECONDS) + try + { + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) .send(); - - Assert.fail(); + Assert.assertThat(response.getStatus(),Matchers.greaterThanOrEqualTo(500)); + } + catch(ExecutionException e) + { + Assert.assertThat(e.getCause(),Matchers.instanceOf(IOException.class)); + } } @Test diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 4a6ad3db3ea..aa45d42d8d3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -27,7 +27,6 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.eclipse.jetty.server.handler.ContextHandler.Context; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.thread.Scheduler; public class AsyncContextEvent extends AsyncEvent implements Runnable @@ -157,7 +156,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable Scheduler.Task task=_timeoutTask; _timeoutTask=null; if (task!=null) - _state.onTimeout(); + _state.getHttpChannel().execute(() -> _state.onTimeout()); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 1cbab46613b..daf1d28888c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -402,10 +402,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { if (!_response.isCommitted() && !_request.isHandled()) _response.sendError(HttpStatus.NOT_FOUND_404); + else if (!_response.isContentComplete(_response.getHttpOutput().getWritten())) + _transport.abort(new IOException("insufficient content written")); _response.closeOutput(); _request.setHandled(true); - _state.onComplete(); + _state.onComplete(); onCompleted(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 5a819b8ef8d..7b7ac90010b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -299,7 +299,7 @@ public class HttpChannelState { listener.onStartAsync(event); } - catch(Exception e) + catch(Throwable e) { // TODO Async Dispatch Error LOG.warn(e); @@ -853,7 +853,7 @@ public class HttpChannelState { listener.onComplete(event); } - catch(Exception e) + catch(Throwable e) { LOG.warn(e+" while invoking onComplete listener " + listener); LOG.debug(e); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 95651d00681..3a9ed7c651e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -524,6 +524,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http @Override public void abort(Throwable failure) { + if (LOG.isDebugEnabled()) + LOG.debug("abort {} {}",this,failure); // Do a direct close of the output, as this may indicate to a client that the // response is bad either with RST or by abnormal completion of chunked response. getEndPoint().close(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 9d1ec89924b..ae37af5f13a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -685,6 +685,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (LOG.isDebugEnabled()) LOG.debug("sendContent({})", BufferUtil.toDetailString(content)); + _written += content.remaining(); write(content, true); closed(); } @@ -766,6 +767,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (LOG.isDebugEnabled()) LOG.debug("sendContent(buffer={},{})", BufferUtil.toDetailString(content), callback); + _written += content.remaining(); write(content, true, new Callback.Nested(callback) { @Override @@ -1280,6 +1282,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable // write what we have _buffer.position(0); _buffer.limit(len); + _written += len; write(_buffer, _eof, this); return Action.SCHEDULED; } @@ -1338,6 +1341,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable // write what we have BufferUtil.flipToFlush(_buffer, 0); + _written += _buffer.remaining(); write(_buffer, _eof, this); return Action.SCHEDULED; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index f3c30f1ee8a..71f96a95f0c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -879,13 +879,13 @@ public class Response implements HttpServletResponse if (isCommitted() || isIncluding()) return; - _contentLength = len; - if (_contentLength > 0) + if (len>0) { long written = _out.getWritten(); if (written > len) throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written); + _contentLength = len; _fields.putLongField(HttpHeader.CONTENT_LENGTH, len); if (isAllContentWritten(written)) { @@ -899,15 +899,19 @@ public class Response implements HttpServletResponse } } } - else if (_contentLength==0) + else if (len==0) { long written = _out.getWritten(); if (written > 0) throw new IllegalArgumentException("setContentLength(0) when already written " + written); + _contentLength = len; _fields.put(HttpHeader.CONTENT_LENGTH, "0"); } else + { + _contentLength = len; _fields.remove(HttpHeader.CONTENT_LENGTH); + } } public long getContentLength() @@ -919,6 +923,11 @@ public class Response implements HttpServletResponse { return (_contentLength >= 0 && written >= _contentLength); } + + public boolean isContentComplete(long written) + { + return (_contentLength < 0 || written >= _contentLength); + } public void closeOutput() throws IOException { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java index 95677401cde..3ddabdbb687 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java @@ -98,6 +98,7 @@ public abstract class AbstractHttpTest writer.write("\r\n"); writer.flush(); + // TODO replace the SimpleHttp stuff SimpleHttpResponse response = httpParser.readResponse(reader); if ("HTTP/1.1".equals(httpVersion) && response.getHeaders().get("content-length") == null diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java index fd4950a18b4..5de23f6ff9e 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; +import java.io.EOFException; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -31,7 +32,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse; +import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -418,6 +421,50 @@ public class HttpManyWaysToCommitTest extends AbstractHttpTest } } + @Test + public void testSetContentLengthFlushAndWriteInsufficientBytes() throws Exception + { + server.setHandler(new SetContentLengthAndWriteInsufficientBytesHandler(true)); + server.start(); + try + { + // TODO This test is compromised by the SimpleHttpResponse mechanism. + // Replace with a better client + + SimpleHttpResponse response = executeRequest(); + String failed_body = ""+(char)-1+(char)-1+(char)-1; + assertThat("response code is 200", response.getCode(), is("200")); + assertThat(response.getBody(), Matchers.endsWith(failed_body)); + assertHeader(response, "content-length", "6"); + } + catch(EOFException e) + { + // possible good response + } + } + + @Test + public void testSetContentLengthAndWriteInsufficientBytes() throws Exception + { + server.setHandler(new SetContentLengthAndWriteInsufficientBytesHandler(false)); + server.start(); + + try + { + // TODO This test is compromised by the SimpleHttpResponse mechanism. + // Replace with a better client + SimpleHttpResponse response = executeRequest(); + String failed_body = ""+(char)-1+(char)-1+(char)-1; + assertThat("response code is 200", response.getCode(), is("200")); + assertThat(response.getBody(), Matchers.endsWith(failed_body)); + assertHeader(response, "content-length", "6"); + } + catch(EOFException e) + { + // expected + } + } + @Test public void testSetContentLengthAndWriteExactlyThatAmountOfBytes() throws Exception { @@ -444,6 +491,25 @@ public class HttpManyWaysToCommitTest extends AbstractHttpTest assertThat("response body is foo", response.getBody(), is("foo")); } + private class SetContentLengthAndWriteInsufficientBytesHandler extends AbstractHandler + { + boolean flush; + private SetContentLengthAndWriteInsufficientBytesHandler(boolean flush) + { + this.flush = flush; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setContentLength(6); + if (flush) + response.flushBuffer(); + response.getWriter().write("foo"); + } + } + private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler { private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException) diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java index f50b2ef9417..ae9b8e36338 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncListenerTest.java @@ -32,12 +32,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.QuietServletException; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.After; +import org.junit.Assert; import org.junit.Test; import static org.hamcrest.Matchers.containsString; @@ -45,12 +47,13 @@ import static org.junit.Assert.assertThat; public class AsyncListenerTest { + private QueuedThreadPool threadPool; private Server server; private LocalConnector connector; public void startServer(ServletContextHandler context) throws Exception { - server = new Server(); + server = threadPool == null ? new Server() : new Server(threadPool); connector = new LocalConnector(server); connector.setIdleTimeout(20 * 60 * 1000L); server.addConnector(connector); @@ -407,6 +410,42 @@ public class AsyncListenerTest assertThat(httpResponse, containsString("DATA")); } + @Test + public void test_StartAsync_OnTimeout_CalledBy_PooledThread() throws Exception + { + String threadNamePrefix = "async_listener"; + threadPool = new QueuedThreadPool(); + threadPool.setName(threadNamePrefix); + ServletContextHandler context = new ServletContextHandler(); + context.addServlet(new ServletHolder(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(1000); + asyncContext.addListener(new AsyncListenerAdapter() + { + @Override + public void onTimeout(AsyncEvent event) throws IOException + { + if (Thread.currentThread().getName().startsWith(threadNamePrefix)) + response.setStatus(HttpStatus.OK_200); + else + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); + asyncContext.complete(); + } + }); + } + }), "/*"); + startServer(context); + + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n")); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } // Unique named RuntimeException to help during debugging / assertions. public static class TestRuntimeException extends RuntimeException diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java index 1110eac8f64..941294b019e 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java @@ -471,6 +471,112 @@ public class AsyncIOServletTest extends AbstractTest assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); } + @Test + public void testAsyncWriteLessThanContentLengthFlushed() throws Exception + { + CountDownLatch complete = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(10); + + AsyncContext async = request.startAsync(); + ServletOutputStream out = response.getOutputStream(); + AtomicInteger state = new AtomicInteger(0); + + out.setWriteListener(new WriteListener() + { + @Override + public void onWritePossible() throws IOException + { + while(true) + { + if (!out.isReady()) + return; + + switch(state.get()) + { + case 0: + state.incrementAndGet(); + WriteListener listener = this; + new Thread(()-> + { + try + { + Thread.sleep(50); + listener.onWritePossible(); + } + catch(Exception e) + {} + }).start(); + return; + + case 1: + state.incrementAndGet(); + out.flush(); + break; + + case 2: + state.incrementAndGet(); + out.write("12345".getBytes()); + break; + + case 3: + async.complete(); + complete.countDown(); + return; + } + } + } + + @Override + public void onError(Throwable t) + { + } + }); + } + }); + + AtomicBoolean failed = new AtomicBoolean(false); + CountDownLatch clientLatch = new CountDownLatch(3); + client.newRequest(newURI()) + .path(servletPath) + .onResponseHeaders(response -> + { + if (response.getStatus() == HttpStatus.OK_200) + clientLatch.countDown(); + }) + .onResponseContent(new Response.ContentListener() + { + @Override + public void onContent(Response response, ByteBuffer content) + { + // System.err.println("Content: "+BufferUtil.toDetailString(content)); + } + }) + .onResponseFailure(new Response.FailureListener() + { + @Override + public void onFailure(Response response, Throwable failure) + { + clientLatch.countDown(); + } + }) + .send(result -> + { + failed.set(result.isFailed()); + clientLatch.countDown(); + clientLatch.countDown(); + clientLatch.countDown(); + }); + + assertTrue(complete.await(10, TimeUnit.SECONDS)); + assertTrue(clientLatch.await(10, TimeUnit.SECONDS)); + assertTrue(failed.get()); + } + @Test public void testIsReadyAtEOF() throws Exception { @@ -979,7 +1085,7 @@ public class AsyncIOServletTest extends AbstractTest while (input.isReady() && !input.isFinished()) { int read = input.read(); - System.err.printf("%x%n", read); + // System.err.printf("%x%n", read); readLatch.countDown(); } }