Merged branch 'jetty-9.4.x' into 'master'.

This commit is contained in:
Simone Bordet 2016-10-28 12:15:46 +02:00
commit b9eb44fab9
14 changed files with 347 additions and 20 deletions

View File

@ -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<SessionTrackingMode>) | setSessionTrackingModes(Set<SessionTrackingMode>)
| 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.

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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)

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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;

View File

@ -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
{

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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();
}
}