From ea39cbef57e5a31602e61920b056f46ffb847209 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 30 Mar 2017 19:29:26 +1100 Subject: [PATCH 01/16] Issue #1418 update after review --- .../main/java/org/eclipse/jetty/server/HttpChannelState.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1075acfdf5d..397de3bbc50 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 @@ -439,6 +439,7 @@ public class HttpChannelState case POSSIBLE: _state=State.ASYNC_IO; + _asyncRead=AsyncRead.PRODUCING; action=Action.READ_PRODUCE; break async; @@ -1269,7 +1270,7 @@ public class HttpChannelState try(Locker.Lock lock= _locker.lock()) { if (LOG.isDebugEnabled()) - LOG.debug("onReadReady {}",toStringLocked()); + LOG.debug("onReadPossible {}",toStringLocked()); switch(_asyncRead) { From 7a6f2860e161e39a9fe05ee4766e09283f8205c5 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 30 Mar 2017 21:59:23 +1100 Subject: [PATCH 02/16] Issue #1418 updates after review --- .../jetty/server/AsyncContextEvent.java | 5 + .../jetty/server/HttpChannelState.java | 132 ++++++++++-------- .../org/eclipse/jetty/server/HttpInput.java | 15 +- .../jetty/server/HttpInputAsyncStateTest.java | 4 +- .../eclipse/jetty/server/HttpInputTest.java | 4 +- 5 files changed, 89 insertions(+), 71 deletions(-) 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 ba90ead0aa1..d4115d6589e 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 @@ -106,6 +106,11 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable _timeoutTask = task; } + public boolean hasTimeoutTask() + { + return _timeoutTask!=null; + } + public void cancelTimeoutTask() { Scheduler.Task task=_timeoutTask; 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 397de3bbc50..bc82936216b 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 @@ -78,7 +78,7 @@ public class HttpChannelState ERROR_DISPATCH, // handle a normal error ASYNC_ERROR, // handle an async error WRITE_CALLBACK, // handle an IO write callback - READ_PRODUCE, // Check is a read is possible by parsing/filling + READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback COMPLETE, // Complete the response TERMINATED, // No further actions @@ -102,13 +102,13 @@ public class HttpChannelState private enum AsyncRead { - NONE, // No isReady; No data - AVAILABLE, // No isReady; onDataAvailable - NEEDED, // isReady()==false handling; No data + IDLE, // No isReady; No data + AVAILABLE, // No isReady; onContentAdded has been called + REGISTER, // isReady()==false handling; No data REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async callback called (http/1 only) - PRODUCING, // isReady()==false handling content production (http/1 only) - READY // isReady() was false, data now available + POSSIBLE, // isReady()==false async read callback called (http/1 only) + PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) + READY // isReady() was false, onContentAdded has been called } private final Locker _locker=new Locker(); @@ -117,7 +117,7 @@ public class HttpChannelState private State _state; private Async _async; private boolean _initial; - private AsyncRead _asyncRead=AsyncRead.NONE; + private AsyncRead _asyncRead=AsyncRead.IDLE; private boolean _asyncWritePossible; private long _timeoutMs=DEFAULT_TIMEOUT; private AsyncContextEvent _event; @@ -237,9 +237,14 @@ public class HttpChannelState return Action.READ_PRODUCE; case READY: _state=State.ASYNC_IO; - _asyncRead=AsyncRead.NONE; + _asyncRead=AsyncRead.IDLE; return Action.READ_CALLBACK; - default: + case REGISTER: + case PRODUCING: + throw new IllegalStateException(toStringLocked()); + case IDLE: + case AVAILABLE: + case REGISTERED: break; } @@ -386,7 +391,6 @@ public class HttpChannelState */ protected Action unhandle() { - Action action; boolean read_interested = false; try(Locker.Lock lock= _locker.lock()) @@ -414,41 +418,38 @@ public class HttpChannelState } _initial=false; - async: switch(_async) + switch(_async) { case COMPLETE: _state=State.COMPLETING; _async=Async.NOT_ASYNC; - action=Action.COMPLETE; - break; + return Action.COMPLETE; case DISPATCH: _state=State.DISPATCHED; _async=Async.NOT_ASYNC; - action=Action.ASYNC_DISPATCH; - break; + return Action.ASYNC_DISPATCH; case STARTED: switch(_asyncRead) { case READY: _state=State.ASYNC_IO; - _asyncRead=AsyncRead.NONE; - action=Action.READ_CALLBACK; - break async; + _asyncRead=AsyncRead.IDLE; + return Action.READ_CALLBACK; case POSSIBLE: _state=State.ASYNC_IO; _asyncRead=AsyncRead.PRODUCING; - action=Action.READ_PRODUCE; - break async; + return Action.READ_PRODUCE; - case NEEDED: + case REGISTER: case PRODUCING: _asyncRead=AsyncRead.REGISTERED; read_interested=true; - - case NONE: + break; + + case IDLE: case AVAILABLE: case REGISTERED: break; @@ -458,54 +459,50 @@ public class HttpChannelState { _state=State.ASYNC_IO; _asyncWritePossible=false; - action=Action.WRITE_CALLBACK; + return Action.WRITE_CALLBACK; } else { _state=State.ASYNC_WAIT; - action=Action.WAIT; + Scheduler scheduler=_channel.getScheduler(); - if (scheduler!=null && _timeoutMs>0) + if (scheduler!=null && _timeoutMs>0 && !_event.hasTimeoutTask()) _event.setTimeoutTask(scheduler.schedule(_event,_timeoutMs,TimeUnit.MILLISECONDS)); + + return Action.WAIT; } - break; case EXPIRING: // onTimeout callbacks still being called, so just WAIT _state=State.ASYNC_WAIT; - action=Action.WAIT; - break; + return Action.WAIT; case EXPIRED: // onTimeout handling is complete, but did not dispatch as // we were handling. So do the error dispatch here _state=State.DISPATCHED; _async=Async.NOT_ASYNC; - action=Action.ERROR_DISPATCH; - break; + return Action.ERROR_DISPATCH; case ERRORED: _state=State.DISPATCHED; _async=Async.NOT_ASYNC; - action=Action.ERROR_DISPATCH; - break; + return Action.ERROR_DISPATCH; case NOT_ASYNC: _state=State.COMPLETING; - action=Action.COMPLETE; - break; + return Action.COMPLETE; default: _state=State.COMPLETING; - action=Action.COMPLETE; - break; + return Action.COMPLETE; } } - - if (read_interested) - _channel.asyncReadFillInterested(); - - return action; + finally + { + if (read_interested) + _channel.asyncReadFillInterested(); + } } public void dispatch(ServletContext context, String path) @@ -931,7 +928,7 @@ public class HttpChannelState _state=State.IDLE; _async=Async.NOT_ASYNC; _initial=true; - _asyncRead=AsyncRead.NONE; + _asyncRead=AsyncRead.IDLE; _asyncWritePossible=false; _timeoutMs=DEFAULT_TIMEOUT; _event=null; @@ -958,7 +955,7 @@ public class HttpChannelState _state=State.UPGRADED; _async=Async.NOT_ASYNC; _initial=true; - _asyncRead=AsyncRead.NONE; + _asyncRead=AsyncRead.IDLE; _asyncWritePossible=false; _timeoutMs=DEFAULT_TIMEOUT; _event=null; @@ -1149,10 +1146,7 @@ public class HttpChannelState switch(_asyncRead) { - case NONE: - case AVAILABLE: - case READY: - case NEEDED: + case IDLE: if (_state==State.ASYNC_WAIT) { interested=true; @@ -1160,16 +1154,18 @@ public class HttpChannelState } else { - _asyncRead=AsyncRead.NEEDED; + _asyncRead=AsyncRead.REGISTER; } break; + case REGISTER: case REGISTERED: case POSSIBLE: case PRODUCING: break; - default: + case AVAILABLE: + case READY: throw new IllegalStateException(toStringLocked()); } } @@ -1185,31 +1181,30 @@ public class HttpChannelState * is returned. * @return True IFF the channel was unready and in ASYNC_WAIT state */ - public boolean onDataAvailable() + public boolean onContentAdded() { boolean woken=false; try(Locker.Lock lock= _locker.lock()) { if (LOG.isDebugEnabled()) - LOG.debug("onReadPossible {}",toStringLocked()); + LOG.debug("onContentAdded {}",toStringLocked()); switch(_asyncRead) { - case NONE: + case IDLE: _asyncRead=AsyncRead.AVAILABLE; break; case AVAILABLE: + case READY: break; case PRODUCING: _asyncRead=AsyncRead.READY; break; - case NEEDED: + case REGISTER: case REGISTERED: - case POSSIBLE: - case READY: _asyncRead=AsyncRead.READY; if (_state==State.ASYNC_WAIT) { @@ -1217,8 +1212,8 @@ public class HttpChannelState _state=State.ASYNC_WOKEN; } break; - - default: + + case POSSIBLE: throw new IllegalStateException(toStringLocked()); } } @@ -1228,7 +1223,7 @@ public class HttpChannelState /** * Called to signal that the channel is ready for a callback. * This is similar to calling {@link #onReadUnready()} followed by - * {@link #onDataAvailable()}, except that as content is already + * {@link #onContentAdded()}, except that as content is already * available, read interest is never set. * @return true if woken */ @@ -1242,7 +1237,7 @@ public class HttpChannelState switch(_asyncRead) { - case NONE: + case IDLE: case AVAILABLE: _asyncRead=AsyncRead.READY; if (_state==State.ASYNC_WAIT) @@ -1290,6 +1285,21 @@ public class HttpChannelState return woken; } + /** + * Called to signal that application has called read. + * @return true if woken + */ + public void onRead(boolean available) + { + try(Locker.Lock lock= _locker.lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("onRead {} {}",available,toStringLocked()); + + _asyncRead=available?AsyncRead.AVAILABLE:AsyncRead.IDLE; + } + } + /** * Called to signal that a read has read -1. * Will wake if the read was called while in ASYNC_WAIT state diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index 135a3d558c5..904aa8e19fe 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -260,9 +260,9 @@ public class HttpInput extends ServletInputStream implements Runnable int l; synchronized (_inputQ) { - // Setup blocking only if not async if (!isAsync()) { + // Setup blocking only if not async if (_blockUntil == 0) { long blockingTimeout = getBlockingTimeout(); @@ -296,7 +296,9 @@ public class HttpInput extends ServletInputStream implements Runnable // Consume any following poison pills if (item.isEmpty()) - nextInterceptedContent(); + _channelState.onRead(nextInterceptedContent()!=null); + else + _channelState.onRead(true); break; } @@ -305,9 +307,10 @@ public class HttpInput extends ServletInputStream implements Runnable { // Not blocking, so what should we return? l = _state.noContent(); + _channelState.onRead(false); - // If EOF do we need to wake for allDataRead callback? if (l<0) + // If EOF do we need to wake for allDataRead callback? wake = _channelState.onReadEof(); break; } @@ -577,7 +580,7 @@ public class HttpInput extends ServletInputStream implements Runnable if (_listener == null) _inputQ.notify(); else - woken = _channelState.onDataAvailable(); + woken = _channelState.onContentAdded(); } return woken; } @@ -612,7 +615,7 @@ public class HttpInput extends ServletInputStream implements Runnable if (_listener == null) _inputQ.notify(); else - woken = _channelState.onDataAvailable(); + woken = _channelState.onContentAdded(); } } return woken; @@ -800,7 +803,7 @@ public class HttpInput extends ServletInputStream implements Runnable if (_listener == null) _inputQ.notify(); else - woken = _channelState.onDataAvailable(); + woken = _channelState.onContentAdded(); } return woken; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java index 79d9d8156d1..c6facb75090 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java @@ -120,9 +120,9 @@ public class HttpInputAsyncStateTest } @Override - public boolean onDataAvailable() + public boolean onContentAdded() { - boolean wake = super.onDataAvailable(); + boolean wake = super.onContentAdded(); __history.add("onReadPossible "+wake); return wake; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java index 3ab156ccfe0..6efcf86a206 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java @@ -111,10 +111,10 @@ public class HttpInputTest } @Override - public boolean onDataAvailable() + public boolean onContentAdded() { _history.add("s.onDataAvailable"); - return super.onDataAvailable(); + return super.onContentAdded(); } @Override From 50c5e7c93e109efc7b2d254118410ec25d11ed6a Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 31 Mar 2017 08:43:15 +1100 Subject: [PATCH 03/16] Issue #1418 updates after review --- .../eclipse/jetty/server/HttpChannelState.java | 17 +---------------- .../org/eclipse/jetty/server/HttpInput.java | 5 +---- 2 files changed, 2 insertions(+), 20 deletions(-) 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 bc82936216b..f740d192f8b 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 @@ -1147,6 +1147,7 @@ public class HttpChannelState switch(_asyncRead) { case IDLE: + case READY: if (_state==State.ASYNC_WAIT) { interested=true; @@ -1165,7 +1166,6 @@ public class HttpChannelState break; case AVAILABLE: - case READY: throw new IllegalStateException(toStringLocked()); } } @@ -1284,21 +1284,6 @@ public class HttpChannelState } return woken; } - - /** - * Called to signal that application has called read. - * @return true if woken - */ - public void onRead(boolean available) - { - try(Locker.Lock lock= _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("onRead {} {}",available,toStringLocked()); - - _asyncRead=available?AsyncRead.AVAILABLE:AsyncRead.IDLE; - } - } /** * Called to signal that a read has read -1. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index 904aa8e19fe..b2efc08d1fe 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -296,9 +296,7 @@ public class HttpInput extends ServletInputStream implements Runnable // Consume any following poison pills if (item.isEmpty()) - _channelState.onRead(nextInterceptedContent()!=null); - else - _channelState.onRead(true); + nextInterceptedContent(); break; } @@ -307,7 +305,6 @@ public class HttpInput extends ServletInputStream implements Runnable { // Not blocking, so what should we return? l = _state.noContent(); - _channelState.onRead(false); if (l<0) // If EOF do we need to wake for allDataRead callback? From 927bf97546028cefabcd494ba702974445875491 Mon Sep 17 00:00:00 2001 From: WalkerWatch Date: Thu, 30 Mar 2017 18:16:28 -0400 Subject: [PATCH 04/16] Resolves #1437 - Updated requestlog ini-template Signed-off-by: WalkerWatch --- jetty-server/src/main/config/modules/requestlog.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-server/src/main/config/modules/requestlog.mod b/jetty-server/src/main/config/modules/requestlog.mod index 3fdb93bd3ab..ef8630eb477 100644 --- a/jetty-server/src/main/config/modules/requestlog.mod +++ b/jetty-server/src/main/config/modules/requestlog.mod @@ -27,7 +27,7 @@ logs/ # jetty.requestlog.retainDays=90 ## Whether to append to existing file -# jetty.requestlog.append=true +# jetty.requestlog.append=false ## Whether to use the extended log output # jetty.requestlog.extended=true From 823cbe1cd4312d50b19d00c590e27997cdee592e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 31 Mar 2017 11:10:10 +1100 Subject: [PATCH 05/16] Issue #1418 simplification after review --- .../org/eclipse/jetty/server/HttpChannelState.java | 11 ----------- 1 file changed, 11 deletions(-) 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 f740d192f8b..6fbc41a9489 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 @@ -103,7 +103,6 @@ public class HttpChannelState private enum AsyncRead { IDLE, // No isReady; No data - AVAILABLE, // No isReady; onContentAdded has been called REGISTER, // isReady()==false handling; No data REGISTERED, // isReady()==false !handling; No data POSSIBLE, // isReady()==false async read callback called (http/1 only) @@ -243,7 +242,6 @@ public class HttpChannelState case PRODUCING: throw new IllegalStateException(toStringLocked()); case IDLE: - case AVAILABLE: case REGISTERED: break; } @@ -450,7 +448,6 @@ public class HttpChannelState break; case IDLE: - case AVAILABLE: case REGISTERED: break; } @@ -1164,9 +1161,6 @@ public class HttpChannelState case POSSIBLE: case PRODUCING: break; - - case AVAILABLE: - throw new IllegalStateException(toStringLocked()); } } @@ -1192,10 +1186,6 @@ public class HttpChannelState switch(_asyncRead) { case IDLE: - _asyncRead=AsyncRead.AVAILABLE; - break; - - case AVAILABLE: case READY: break; @@ -1238,7 +1228,6 @@ public class HttpChannelState switch(_asyncRead) { case IDLE: - case AVAILABLE: _asyncRead=AsyncRead.READY; if (_state==State.ASYNC_WAIT) { From f06c7c358f23674d33b629b3d5f402042ffd4656 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 1 Apr 2017 09:18:37 +1100 Subject: [PATCH 06/16] Fixes #1440 Improve low resource solution for scheduling strategy. Replaced the dual scheduling strategy with a single re-implementation of EatWhatYouKill that can adapt to act as ProduceConsume, ExectureProduceConsume or ProduceExecuteConsume as need be. Squashed commit of the following: commit 25eeb32181549a39ef338b8858c20a534da91552 Author: Greg Wilkins Date: Sat Apr 1 09:08:49 2017 +1100 renamed variables commit 4f370d84cab21db5d0489dcd622954853b992d70 Merge: 8159c50 823cbe1 Author: Greg Wilkins Date: Fri Mar 31 11:54:26 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 8159c508f057573a20e803579e1d237083dad51c Merge: 5805a92 daf61cd Author: Greg Wilkins Date: Thu Mar 30 17:28:00 2017 +1100 Merge remote-tracking branch 'origin/jetty-9.3.x' into jetty-9.4.x-ewyk commit daf61cd2942abe93e6f3d728e5220691d08a9372 Author: Denis Nedelyaev Date: Thu Mar 30 04:15:32 2017 +0300 Fix memory leak in GzipHandler (#1429) Looks like this leak shows itself only with asynchronous responses. Some relevant info: http://www.devguli.com/blog/eng/java-deflater-and-outofmemoryerror/ Signed-off-by: Denis Nedelyaev commit 5805a92d0ec968ab6f70d5acf3668954f14ddaba Merge: cfabbd2 dc759db Author: Greg Wilkins Date: Thu Mar 30 17:12:38 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit cfabbd23413aaeb6158262efb4b45eabbe51daa5 Author: Greg Wilkins Date: Thu Mar 30 16:04:57 2017 +1100 minor cleanups commit c7aa64aa1080cccabbffbc12cdd33b5ac7b867f6 Merge: bacf51a 18f17ac Author: Greg Wilkins Date: Thu Mar 30 14:58:37 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit bacf51a83d8e3b41c977ba3a713223e53bd3ce07 Merge: 11ba4bc 2fafa1d Author: Greg Wilkins Date: Thu Mar 30 14:13:36 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 11ba4bc8497e746a8a62f1768c557f5606e85f2c Merge: 69003d3 1a0b2df Author: Greg Wilkins Date: Thu Mar 30 13:48:09 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 69003d3dd94b19197681d231729277fad33922a2 Merge: f89b08d a8ff18d Author: Greg Wilkins Date: Thu Mar 30 12:35:27 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit f89b08d08a6e67ce1e321db6fc58904c4e89969c Merge: 7a87c8e 00b42ca Author: Greg Wilkins Date: Thu Mar 23 16:01:00 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 7a87c8ec9df9fea1884024624a5b14235889679f Merge: 1a92015 12dc169 Author: Greg Wilkins Date: Thu Mar 23 10:27:14 2017 +1100 Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-ewyk commit 1a92015471dd0e0334f5968c91a540c62a3d9638 Author: Greg Wilkins Date: Tue Mar 21 09:23:53 2017 +1100 better spruious wakeup handling and other simplifications commit c01a910b69c145568320f3f8ee138880fc26d8d1 Merge: 0b2b9ea 67ec4b0 Author: Greg Wilkins Date: Fri Mar 17 14:59:37 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 0b2b9ea187b444ab9298069d7d46a989d8973d06 Author: Greg Wilkins Date: Fri Mar 17 14:52:49 2017 +1100 cleanup commit c1d92ebde6780d570a9a23cd5a09406691c56389 Author: Greg Wilkins Date: Fri Mar 17 13:41:45 2017 +1100 Fixed push commit d2d6bc3e65329d25fbd9331df58e8bfaccb027bc Author: Greg Wilkins Date: Fri Mar 17 12:18:03 2017 +1100 minor cleanups commit c1a159b7a9f5e4b1eaea5c4d274427724307523c Merge: 01349ac 78f4712 Author: Greg Wilkins Date: Fri Mar 17 09:30:44 2017 +1100 Merge branch 'jetty-9.4.x' into jetty-9.4.x-ewyk commit 01349ac93573d46f5c5bb31a590fa2b7a846cff0 Merge: 4dc1503 08f351b Author: Greg Wilkins Date: Fri Mar 17 08:16:06 2017 +1100 Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-ewyk commit 4dc15032ba81c38670dfc42a34603caa6080c21c Author: Greg Wilkins Date: Thu Mar 16 23:26:59 2017 +1100 work in progress commit 5d18c654b8a834524cb04549bfa0fcf4d905805e Author: Greg Wilkins Date: Thu Mar 16 22:05:03 2017 +1100 work in progress commit d52a09a6cdb973f10d03364cbd37ed50d94ce922 Author: Greg Wilkins Date: Thu Mar 16 18:03:16 2017 +1100 work in progress commit c097db32e7855cbe36fd269596716c4d44930cd9 Author: Greg Wilkins Date: Thu Mar 16 15:59:29 2017 +1100 Experiement enhancement to EatWhatYouKill ExecutionStrategy Use the existence of a pending producer threads to determine if low resources or not. --- .../jetty/http2/client/HTTP2Client.java | 1 - .../eclipse/jetty/http2/HTTP2Connection.java | 49 +-- .../org/eclipse/jetty/io/ManagedSelector.java | 100 +---- .../server/handler/gzip/GzipHandler.java | 6 +- .../org/eclipse/jetty/util/BufferUtil.java | 8 +- .../util/component/AbstractLifeCycle.java | 2 +- .../jetty/util/component/LifeCycle.java | 43 ++ .../jetty/util/thread/ExecutionStrategy.java | 52 +-- .../eclipse/jetty/util/thread/Invocable.java | 89 ++++- .../util/thread/strategy/EatWhatYouKill.java | 374 ++++++++++++++++++ .../strategy/ExecuteProduceConsume.java | 23 +- .../strategy/ExecutingExecutionStrategy.java | 88 ----- .../util/thread/strategy/ProduceConsume.java | 9 - .../strategy/ProduceExecuteConsume.java | 25 +- .../strategy/ExecutionStrategyTest.java | 218 ++++++++++ .../test/resources/jetty-logging.properties | 2 + 16 files changed, 782 insertions(+), 307 deletions(-) create mode 100644 jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java delete mode 100644 jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecutingExecutionStrategy.java create mode 100644 jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java index fc9655d9273..f2c33bf3e63 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java @@ -129,7 +129,6 @@ public class HTTP2Client extends ContainerLifeCycle private int initialSessionRecvWindow = FlowControlStrategy.DEFAULT_WINDOW_SIZE; private int initialStreamRecvWindow = FlowControlStrategy.DEFAULT_WINDOW_SIZE; private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F); - private ExecutionStrategy.Factory executionStrategyFactory = new ProduceConsume.Factory(); @Override protected void doStart() throws Exception diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index 3c696ac1a28..65a04cbd0e6 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -31,12 +31,11 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ExecutionStrategy; -import org.eclipse.jetty.util.thread.Invocable; -import org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume; -import org.eclipse.jetty.util.thread.strategy.ProduceExecuteConsume; +import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill; public class HTTP2Connection extends AbstractConnection { @@ -49,8 +48,7 @@ public class HTTP2Connection extends AbstractConnection private final Parser parser; private final ISession session; private final int bufferSize; - private final ExecutionStrategy blockingStrategy; - private final ExecutionStrategy nonBlockingStrategy; + private final ExecutionStrategy strategy; public HTTP2Connection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, Parser parser, ISession session, int bufferSize) { @@ -59,8 +57,9 @@ public class HTTP2Connection extends AbstractConnection this.parser = parser; this.session = session; this.bufferSize = bufferSize; - this.blockingStrategy = new ExecuteProduceConsume(producer, executor); - this.nonBlockingStrategy = new ProduceExecuteConsume(producer, executor); + this.strategy = new EatWhatYouKill(producer, executor, 0); + + LifeCycle.start(strategy); } @Override @@ -96,7 +95,7 @@ public class HTTP2Connection extends AbstractConnection if (LOG.isDebugEnabled()) LOG.debug("HTTP2 Open {} ", this); super.onOpen(); - blockingStrategy.produce(); + strategy.produce(); } @Override @@ -105,26 +104,16 @@ public class HTTP2Connection extends AbstractConnection if (LOG.isDebugEnabled()) LOG.debug("HTTP2 Close {} ", this); super.onClose(); + + LifeCycle.stop(strategy); } @Override public void onFillable() - { - throw new UnsupportedOperationException(); - } - - private void onFillableBlocking() { if (LOG.isDebugEnabled()) - LOG.debug("HTTP2 onFillableBlocking {} ", this); - blockingStrategy.produce(); - } - - private void onFillableNonBlocking() - { - if (LOG.isDebugEnabled()) - LOG.debug("HTTP2 onFillableNonBlocking {} ", this); - nonBlockingStrategy.produce(); + LOG.debug("HTTP2 onFillable {} ", this); + strategy.produce(); } private int fill(EndPoint endPoint, ByteBuffer buffer) @@ -158,16 +147,7 @@ public class HTTP2Connection extends AbstractConnection protected void offerTask(Runnable task, boolean dispatch) { offerTask(task); - - // Because producing calls parse and parse can call offerTask, we have to make sure - // we use the same strategy otherwise produce can be reentrant and that messes with - // the release mechanism. TODO is this test sufficient to protect from this? - ExecutionStrategy s = Invocable.isNonBlockingInvocation() ? nonBlockingStrategy : blockingStrategy; - if (dispatch) - // TODO Why again is this necessary? - s.dispatch(); - else - s.produce(); + strategy.dispatch(); } @Override @@ -271,10 +251,7 @@ public class HTTP2Connection extends AbstractConnection @Override public void succeeded() { - if (Invocable.isNonBlockingInvocation()) - onFillableNonBlocking(); - else - onFillableBlocking(); + onFillable(); } @Override diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index d808f7f8fe2..f8112ddedee 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -48,6 +48,7 @@ import org.eclipse.jetty.util.thread.Invocable; import org.eclipse.jetty.util.thread.Invocable.InvocationType; import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill; import org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume; import org.eclipse.jetty.util.thread.strategy.ProduceExecuteConsume; @@ -57,7 +58,7 @@ import org.eclipse.jetty.util.thread.strategy.ProduceExecuteConsume; * happen for registered channels. When events happen, it notifies the {@link EndPoint} associated * with the channel.

*/ -public class ManagedSelector extends AbstractLifeCycle implements Dumpable +public class ManagedSelector extends ContainerLifeCycle implements Dumpable { private static final Logger LOG = Log.getLogger(ManagedSelector.class); @@ -67,7 +68,6 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable private final SelectorManager _selectorManager; private final int _id; private final ExecutionStrategy _strategy; - private final ExecutionStrategy _lowPriorityStrategy; private Selector _selector; public ManagedSelector(SelectorManager selectorManager, int id) @@ -76,8 +76,8 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable _id = id; SelectorProducer producer = new SelectorProducer(); Executor executor = selectorManager.getExecutor(); - _strategy = new ExecuteProduceConsume(producer, executor, Invocable.InvocationType.BLOCKING); - _lowPriorityStrategy = new LowPriorityProduceExecuteConsume(producer, executor); + _strategy = new EatWhatYouKill(producer,executor); + addBean(_strategy); setStopTimeout(5000); } @@ -94,29 +94,6 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable // The normal strategy obtains the produced task, schedules // a new thread to produce more, runs the task and then exits. _selectorManager.execute(_strategy::produce); - - // The low priority strategy knows the producer will never - // be idle, that tasks are scheduled to run in different - // threads, therefore lowPriorityProduce() never exits. - _selectorManager.execute(this::lowPriorityProduce); - } - - private void lowPriorityProduce() - { - Thread current = Thread.currentThread(); - String name = current.getName(); - int priority = current.getPriority(); - current.setPriority(Thread.MIN_PRIORITY); - current.setName(name+"-lowPrioritySelector"); - try - { - _lowPriorityStrategy.produce(); - } - finally - { - current.setPriority(priority); - current.setName(name); - } } public int size() @@ -135,11 +112,12 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable CloseEndPoints close_endps = new CloseEndPoints(); submit(close_endps); close_endps.await(getStopTimeout()); - super.doStop(); CloseSelector close_selector = new CloseSelector(); submit(close_selector); close_selector.await(getStopTimeout()); + super.doStop(); + if (LOG.isDebugEnabled()) LOG.debug("Stopped {}", this); } @@ -185,42 +163,6 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable void updateKey(); } - private static class LowPriorityProduceExecuteConsume extends ProduceExecuteConsume - { - private LowPriorityProduceExecuteConsume(SelectorProducer producer, Executor executor) - { - super(producer, executor, InvocationType.BLOCKING); - } - - @Override - protected boolean execute(Runnable task) - { - try - { - InvocationType invocation=Invocable.getInvocationType(task); - if (LOG.isDebugEnabled()) - LOG.debug("Low Priority Selector executing {} {}",invocation,task); - switch (invocation) - { - case NON_BLOCKING: - task.run(); - return true; - - case EITHER: - Invocable.invokeNonBlocking(task); - return true; - - default: - return super.execute(task); - } - } - finally - { - // Allow opportunity for main strategy to take over. - Thread.yield(); - } - } - } private class SelectorProducer implements ExecutionStrategy.Producer { @@ -230,30 +172,20 @@ public class ManagedSelector extends AbstractLifeCycle implements Dumpable @Override public Runnable produce() { - // This method is called from both the - // normal and low priority strategies. - // Only one can produce at a time, so it's synchronized - // to enforce that only one strategy actually produces. - // When idle in select(), this method blocks; - // the other strategy's thread will be blocked - // waiting for this lock to be released. - synchronized (this) + while (true) { - while (true) - { - Runnable task = processSelected(); - if (task != null) - return task; + Runnable task = processSelected(); + if (task != null) + return task; - Runnable action = nextAction(); - if (action != null) - return action; + Runnable action = nextAction(); + if (action != null) + return action; - update(); + update(); - if (!select()) - return null; - } + if (!select()) + return null; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index 41447f752a2..9e6c6a74da1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -561,9 +561,13 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory @Override public void recycle(Deflater deflater) { - deflater.reset(); if (_deflater.get()==null) + { + deflater.reset(); _deflater.set(deflater); + } + else + deflater.end(); } /** diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index efc3d582e43..0201a174d13 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -420,11 +420,11 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** * Like append, but does not throw {@link BufferOverflowException} - * @param to Buffer is flush mode - * @param b bytes to fill - * @param off offset into byte + * @param to Buffer The buffer to fill to. The buffer will be flipped to fill mode and then flipped back to flush mode. + * @param b bytes The bytes to fill + * @param off offset into bytes * @param len length to fill - * @return The position of the valid data before the flipped position. + * @return the number of bytes taken from the buffer. */ public static int fill(ByteBuffer to, byte[] b, int off, int len) { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java index 402a58cbcbc..f9a9894501c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java @@ -225,7 +225,7 @@ public abstract class AbstractLifeCycle implements LifeCycle { this._stopTimeout = stopTimeout; } - + public static abstract class AbstractLifeCycleListener implements LifeCycle.Listener { @Override public void lifeCycleFailure(LifeCycle event, Throwable cause) {} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java index a6e0cb2cbf0..4d2bd2dadfc 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/LifeCycle.java @@ -122,4 +122,47 @@ public interface LifeCycle public void lifeCycleStopping(LifeCycle event); public void lifeCycleStopped(LifeCycle event); } + + + /** + * Utility to start an object if it is a LifeCycle and to convert + * any exception thrown to a {@link RuntimeException} + * @param object The instance to start. + * @throws RuntimeException if the call to start throws an exception. + */ + public static void start(Object object) + { + if (object instanceof LifeCycle) + { + try + { + ((LifeCycle)object).start(); + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + } + + /** + * Utility to stop an object if it is a LifeCycle and to convert + * any exception thrown to a {@link RuntimeException} + * @param object The instance to stop. + * @throws RuntimeException if the call to stop throws an exception. + */ + public static void stop(Object object) + { + if (object instanceof LifeCycle) + { + try + { + ((LifeCycle)object).stop(); + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java index fe1ab63684b..a5fb26e267a 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutionStrategy.java @@ -25,6 +25,7 @@ import java.util.concurrent.RejectedExecutionException; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill; import org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume; /** @@ -73,55 +74,4 @@ public interface ExecutionStrategy Runnable produce(); } - - /** - *

A factory for {@link ExecutionStrategy}.

- */ - public static interface Factory - { - /** - *

Creates a new {@link ExecutionStrategy}.

- * - * @param producer the execution strategy producer - * @param executor the execution strategy executor - * @return a new {@link ExecutionStrategy} - */ - public ExecutionStrategy newExecutionStrategy(Producer producer, Executor executor); - - /** - * @return the default {@link ExecutionStrategy} - */ - public static Factory getDefault() - { - return DefaultExecutionStrategyFactory.INSTANCE; - } - } - - public static class DefaultExecutionStrategyFactory implements Factory - { - private static final Logger LOG = Log.getLogger(Factory.class); - private static final Factory INSTANCE = new DefaultExecutionStrategyFactory(); - - @Override - public ExecutionStrategy newExecutionStrategy(Producer producer, Executor executor) - { - String strategy = System.getProperty(producer.getClass().getName() + ".ExecutionStrategy"); - if (strategy != null) - { - try - { - Class c = Loader.loadClass(strategy); - Constructor m = c.getConstructor(Producer.class, Executor.class); - LOG.info("Use {} for {}", c.getSimpleName(), producer.getClass().getName()); - return m.newInstance(producer, executor); - } - catch (Exception e) - { - LOG.warn(e); - } - } - - return new ExecuteProduceConsume(producer, executor); - } - } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java index 9875f8c0823..cb926417b28 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/Invocable.java @@ -18,7 +18,13 @@ package org.eclipse.jetty.util.thread; +import java.io.Closeable; import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; /** *

A task (typically either a {@link Runnable} or {@link Callable} @@ -151,7 +157,7 @@ public interface Invocable break; case EITHER: - if (getInvocationType(task) == InvocationType.EITHER && preferredInvocationType == InvocationType.NON_BLOCKING) + if (preferredInvocationType == InvocationType.NON_BLOCKING) return () -> invokeNonBlocking(task); break; } @@ -179,4 +185,85 @@ public interface Invocable { return InvocationType.BLOCKING; } + + /** + * An Executor wrapper that knows about Invocable + * + */ + public static class InvocableExecutor implements Executor + { + private static final Logger LOG = Log.getLogger(InvocableExecutor.class); + + private final Executor _executor; + private final InvocationType _preferredInvocationForExecute; + private final InvocationType _preferredInvocationForInvoke; + + public InvocableExecutor(Executor executor,InvocationType preferred) + { + this(executor,preferred,preferred); + } + + public InvocableExecutor(Executor executor,InvocationType preferredInvocationForExecute,InvocationType preferredInvocationForIvoke) + { + _executor=executor; + _preferredInvocationForExecute=preferredInvocationForExecute; + _preferredInvocationForInvoke=preferredInvocationForIvoke; + } + + public Invocable.InvocationType getPreferredInvocationType() + { + return _preferredInvocationForInvoke; + } + + public void invoke(Runnable task) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} invoke {}", this, task); + Invocable.invokePreferred(task,_preferredInvocationForInvoke); + if (LOG.isDebugEnabled()) + LOG.debug("{} invoked {}", this, task); + } + + public void execute(Runnable task) + { + tryExecute(task,_preferredInvocationForExecute); + } + + public void execute(Runnable task, InvocationType preferred) + { + tryExecute(task,preferred); + } + + public boolean tryExecute(Runnable task) + { + return tryExecute(task,_preferredInvocationForExecute); + } + + public boolean tryExecute(Runnable task, InvocationType preferred) + { + try + { + _executor.execute(Invocable.asPreferred(task,preferred)); + return true; + } + catch(RejectedExecutionException e) + { + // If we cannot execute, then close the task + LOG.debug(e); + LOG.warn("Rejected execution of {}",task); + try + { + if (task instanceof Closeable) + ((Closeable)task).close(); + } + catch (Exception x) + { + e.addSuppressed(x); + LOG.warn(e); + } + } + return false; + } + + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java new file mode 100644 index 00000000000..84898e4fecb --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/EatWhatYouKill.java @@ -0,0 +1,374 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread.strategy; + +import java.util.concurrent.Executor; +import java.util.concurrent.locks.Condition; + +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.ExecutionStrategy; +import org.eclipse.jetty.util.thread.Invocable; +import org.eclipse.jetty.util.thread.Invocable.InvocableExecutor; +import org.eclipse.jetty.util.thread.Invocable.InvocationType; +import org.eclipse.jetty.util.thread.Locker; +import org.eclipse.jetty.util.thread.Locker.Lock; + +/** + *

A strategy where the thread that produces will run the resulting task if it + * is possible to do so without thread starvation.

+ * + *

This strategy preemptively dispatches a thread as a pending producer, so that + * when a thread produces a task it can immediately run the task and let the pending + * producer thread take over producing. If necessary another thread will be dispatched + * to replace the pending producing thread. When operating in this pattern, the + * sub-strategy is called Execute Produce Consume (EPC) + *

+ *

However, if the task produced uses the {@link Invocable} API to indicate that + * it will not block, then the strategy will run it directly, regardless of the + * presence of a pending producing thread and then resume producing after the + * task has completed. This sub-strategy is also used if the strategy has been + * configured with a maximum of 0 pending threads and the thread currently producing + * does not use the {@link Invocable} API to indicate that it will not block. + * When operating in this pattern, the sub-strategy is called + * ProduceConsume (PC). + *

+ *

If there is no pending producer thread available and if the task has not + * indicated it is non-blocking, then this strategy will dispatch the execution of + * the task and immediately continue producing. When operating in this pattern, the + * sub-strategy is called ProduceExecuteConsume (PEC). + *

+ * + */ +public class EatWhatYouKill extends AbstractLifeCycle implements ExecutionStrategy, Runnable +{ + private static final Logger LOG = Log.getLogger(EatWhatYouKill.class); + + enum State { IDLE, PRODUCING, REPRODUCING }; + + private final Locker _locker = new Locker(); + private State _state = State.IDLE; + private final Runnable _runProduce = new RunProduce(); + private final Producer _producer; + private final InvocableExecutor _executor; + private int _pendingProducersMax; + private int _pendingProducers; + private int _pendingProducersDispatched; + private int _pendingProducersSignalled; + private Condition _produce = _locker.newCondition(); + + public EatWhatYouKill(Producer producer, Executor executor) + { + this(producer,executor,InvocationType.NON_BLOCKING,InvocationType.BLOCKING); + } + + public EatWhatYouKill(Producer producer, Executor executor, int maxProducersPending ) + { + this(producer,executor,InvocationType.NON_BLOCKING,InvocationType.BLOCKING); + } + + public EatWhatYouKill(Producer producer, Executor executor, InvocationType preferredInvocationPEC, InvocationType preferredInvocationEPC) + { + this(producer,executor,preferredInvocationPEC,preferredInvocationEPC,Integer.getInteger("org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.maxProducersPending",1)); + } + + public EatWhatYouKill(Producer producer, Executor executor, InvocationType preferredInvocationPEC, InvocationType preferredInvocationEPC, int maxProducersPending ) + { + _producer = producer; + _pendingProducersMax = maxProducersPending; + _executor = new InvocableExecutor(executor,preferredInvocationPEC,preferredInvocationEPC); + } + + @Override + public void produce() + { + boolean produce; + try (Lock locked = _locker.lock()) + { + switch(_state) + { + case IDLE: + _state = State.PRODUCING; + produce = true; + break; + + case PRODUCING: + _state = State.REPRODUCING; + produce = false; + break; + + default: + produce = false; + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("{} execute {}", this, produce); + + if (produce) + doProduce(); + } + + @Override + public void dispatch() + { + boolean dispatch = false; + try (Lock locked = _locker.lock()) + { + switch(_state) + { + case IDLE: + dispatch = true; + break; + + case PRODUCING: + _state = State.REPRODUCING; + dispatch = false; + break; + + default: + dispatch = false; + } + } + if (LOG.isDebugEnabled()) + LOG.debug("{} dispatch {}", this, dispatch); + if (dispatch) + _executor.execute(_runProduce,InvocationType.BLOCKING); + } + + @Override + public void run() + { + if (LOG.isDebugEnabled()) + LOG.debug("{} run", this); + if (!isRunning()) + return; + boolean producing = false; + try (Lock locked = _locker.lock()) + { + _pendingProducersDispatched--; + _pendingProducers++; + + loop: while (isRunning()) + { + try + { + _produce.await(); + + if (_pendingProducersSignalled==0) + { + // spurious wakeup! + continue loop; + } + + _pendingProducersSignalled--; + if (_state == State.IDLE) + { + _state = State.PRODUCING; + producing = true; + } + } + catch (InterruptedException e) + { + LOG.debug(e); + _pendingProducers--; + } + + break loop; + } + } + + if (producing) + doProduce(); + } + + private void doProduce() + { + boolean may_block_caller = !Invocable.isNonBlockingInvocation(); + if (LOG.isDebugEnabled()) + LOG.debug("{} produce {}", this,may_block_caller?"non-blocking":"blocking"); + + producing: while (isRunning()) + { + // If we got here, then we are the thread that is producing. + Runnable task = _producer.produce(); + + boolean produce; + boolean consume; + boolean execute_producer; + + StringBuilder state = null; + + try (Lock locked = _locker.lock()) + { + if (LOG.isDebugEnabled()) + { + state = new StringBuilder(); + getString(state); + getState(state); + state.append("->"); + } + + // Did we produced a task? + if (task == null) + { + // There is no task. + // Could another one just have been queued with a produce call? + if (_state==State.REPRODUCING) + { + _state = State.PRODUCING; + continue producing; + } + + // ... and no additional calls to execute, so we are idle + _state = State.IDLE; + break producing; + } + + // Will we eat our own kill - ie consume the task we just produced? + if (Invocable.getInvocationType(task)==InvocationType.NON_BLOCKING) + { + // ProduceConsume + produce = true; + consume = true; + execute_producer = false; + } + else if (may_block_caller && (_pendingProducers>0 || _pendingProducersMax==0)) + { + // ExecuteProduceConsume (eat what we kill!) + produce = false; + consume = true; + execute_producer = true; + _pendingProducersDispatched++; + _state = State.IDLE; + _pendingProducers--; + _pendingProducersSignalled++; + _produce.signal(); + } + else + { + // ProduceExecuteConsume + produce = true; + consume = false; + execute_producer = (_pendingProducersDispatched + _pendingProducers)<_pendingProducersMax; + if (execute_producer) + _pendingProducersDispatched++; + } + + if (LOG.isDebugEnabled()) + getState(state); + + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("{} {} {}", + state, + consume?(execute_producer?"EPC!":"PC"):"PEC", + task); + } + + if (execute_producer) + // Spawn a new thread to continue production by running the produce loop. + _executor.execute(this); + + // Run or execute the task. + if (consume) + _executor.invoke(task); + else + _executor.execute(task); + + // Once we have run the task, we can try producing again. + if (produce) + continue producing; + + try (Lock locked = _locker.lock()) + { + if (_state==State.IDLE) + { + _state = State.PRODUCING; + continue producing; + } + } + + break producing; + } + if (LOG.isDebugEnabled()) + LOG.debug("{} produce exit",this); + } + + public Boolean isIdle() + { + try (Lock locked = _locker.lock()) + { + return _state==State.IDLE; + } + } + + @Override + protected void doStop() throws Exception + { + try (Lock locked = _locker.lock()) + { + _pendingProducersSignalled=_pendingProducers+_pendingProducersDispatched; + _pendingProducers=0; + _produce.signalAll(); + } + } + + public String toString() + { + StringBuilder builder = new StringBuilder(); + getString(builder); + try (Lock locked = _locker.lock()) + { + getState(builder); + } + return builder.toString(); + } + + private void getString(StringBuilder builder) + { + builder.append(getClass().getSimpleName()); + builder.append('@'); + builder.append(Integer.toHexString(hashCode())); + builder.append('/'); + builder.append(_producer); + builder.append('/'); + } + + private void getState(StringBuilder builder) + { + builder.append(_state); + builder.append('/'); + builder.append(_pendingProducers); + builder.append('/'); + builder.append(_pendingProducersMax); + } + + private class RunProduce implements Runnable + { + @Override + public void run() + { + produce(); + } + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java index 70a4f352fb0..1a8d3666df6 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecuteProduceConsume.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ExecutionStrategy; import org.eclipse.jetty.util.thread.Invocable; +import org.eclipse.jetty.util.thread.Invocable.InvocableExecutor; import org.eclipse.jetty.util.thread.Invocable.InvocationType; import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Locker.Lock; @@ -41,13 +42,14 @@ import org.eclipse.jetty.util.thread.Locker.Lock; * does not yet have capacity to consume, which can save memory and exert back * pressure on producers.

*/ -public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements ExecutionStrategy, Runnable +public class ExecuteProduceConsume implements ExecutionStrategy, Runnable { private static final Logger LOG = Log.getLogger(ExecuteProduceConsume.class); private final Locker _locker = new Locker(); private final Runnable _runProduce = new RunProduce(); private final Producer _producer; + private final InvocableExecutor _executor; private boolean _idle = true; private boolean _execute; private boolean _producing; @@ -61,10 +63,10 @@ public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements public ExecuteProduceConsume(Producer producer, Executor executor, InvocationType preferred ) { - super(executor,preferred); this._producer = producer; + _executor = new InvocableExecutor(executor,preferred); } - + @Override public void produce() { @@ -111,7 +113,7 @@ public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements _execute = true; } if (dispatch) - execute(_runProduce); + _executor.execute(_runProduce); } @Override @@ -190,7 +192,7 @@ public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements // Spawn a new thread to continue production by running the produce loop. if (LOG.isDebugEnabled()) LOG.debug("{} dispatch", this); - if (!execute(this)) + if (!_executor.tryExecute(this)) task = null; } @@ -198,7 +200,7 @@ public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements if (LOG.isDebugEnabled()) LOG.debug("{} run {}", this, task); if (task != null) - invoke(task); + _executor.invoke(task); if (LOG.isDebugEnabled()) LOG.debug("{} ran {}", this, task); @@ -247,13 +249,4 @@ public class ExecuteProduceConsume extends ExecutingExecutionStrategy implements produce(); } } - - public static class Factory implements ExecutionStrategy.Factory - { - @Override - public ExecutionStrategy newExecutionStrategy(Producer producer, Executor executor) - { - return new ExecuteProduceConsume(producer, executor); - } - } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecutingExecutionStrategy.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecutingExecutionStrategy.java deleted file mode 100644 index 45b308d63be..00000000000 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ExecutingExecutionStrategy.java +++ /dev/null @@ -1,88 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.util.thread.strategy; - -import java.io.Closeable; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; - -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.ExecutionStrategy; -import org.eclipse.jetty.util.thread.Invocable; - -/** - *

Base class for strategies that need to execute a task by submitting it to an {@link Executor}.

- *

If the submission to the {@code Executor} is rejected (via a {@link RejectedExecutionException}), - * the task is tested whether it implements {@link Closeable}; if it does, then {@link Closeable#close()} - * is called on the task object.

- */ -public abstract class ExecutingExecutionStrategy implements ExecutionStrategy -{ - private static final Logger LOG = Log.getLogger(ExecutingExecutionStrategy.class); - - private final Executor _executor; - private final Invocable.InvocationType _preferredInvocationType; - - protected ExecutingExecutionStrategy(Executor executor,Invocable.InvocationType preferred) - { - _executor=executor; - _preferredInvocationType=preferred; - } - - public Invocable.InvocationType getPreferredInvocationType() - { - return _preferredInvocationType; - } - - public void invoke(Runnable task) - { - if (LOG.isDebugEnabled()) - LOG.debug("{} invoke {}", this, task); - Invocable.invokePreferred(task,_preferredInvocationType); - if (LOG.isDebugEnabled()) - LOG.debug("{} invoked {}", this, task); - } - - protected boolean execute(Runnable task) - { - try - { - _executor.execute(Invocable.asPreferred(task,_preferredInvocationType)); - return true; - } - catch(RejectedExecutionException e) - { - // If we cannot execute, then close the task and keep producing. - LOG.debug(e); - LOG.warn("Rejected execution of {}",task); - try - { - if (task instanceof Closeable) - ((Closeable)task).close(); - } - catch (Exception x) - { - e.addSuppressed(x); - LOG.warn(e); - } - } - return false; - } -} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java index efb0141e0af..63b806ad22e 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceConsume.java @@ -106,15 +106,6 @@ public class ProduceConsume implements ExecutionStrategy, Runnable produce(); } - public static class Factory implements ExecutionStrategy.Factory - { - @Override - public ExecutionStrategy newExecutionStrategy(Producer producer, Executor executor) - { - return new ProduceConsume(producer, executor); - } - } - private enum State { IDLE, PRODUCE, EXECUTE diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java index e32095d705e..4e14596bc45 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/ProduceExecuteConsume.java @@ -23,7 +23,8 @@ import java.util.concurrent.Executor; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ExecutionStrategy; -import org.eclipse.jetty.util.thread.Invocable; +import org.eclipse.jetty.util.thread.Invocable.InvocationType; +import org.eclipse.jetty.util.thread.Invocable.InvocableExecutor; import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Locker.Lock; @@ -31,23 +32,24 @@ import org.eclipse.jetty.util.thread.Locker.Lock; *

A strategy where the caller thread iterates over task production, submitting each * task to an {@link Executor} for execution.

*/ -public class ProduceExecuteConsume extends ExecutingExecutionStrategy implements ExecutionStrategy +public class ProduceExecuteConsume implements ExecutionStrategy { private static final Logger LOG = Log.getLogger(ProduceExecuteConsume.class); private final Locker _locker = new Locker(); private final Producer _producer; + private final InvocableExecutor _executor; private State _state = State.IDLE; public ProduceExecuteConsume(Producer producer, Executor executor) { - this(producer,executor,Invocable.InvocationType.NON_BLOCKING); + this(producer,executor,InvocationType.NON_BLOCKING); } - public ProduceExecuteConsume(Producer producer, Executor executor, Invocable.InvocationType preferred) + public ProduceExecuteConsume(Producer producer, Executor executor, InvocationType preferred) { - super(executor,preferred); - this._producer = producer; + _producer = producer; + _executor = new InvocableExecutor(executor,preferred); } @Override @@ -95,7 +97,7 @@ public class ProduceExecuteConsume extends ExecutingExecutionStrategy implements } // Execute the task. - execute(task); + _executor.execute(task); } } @@ -105,15 +107,6 @@ public class ProduceExecuteConsume extends ExecutingExecutionStrategy implements produce(); } - public static class Factory implements ExecutionStrategy.Factory - { - @Override - public ExecutionStrategy newExecutionStrategy(Producer producer, Executor executor) - { - return new ProduceExecuteConsume(producer, executor); - } - } - private enum State { IDLE, PRODUCE, EXECUTE diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java new file mode 100644 index 00000000000..23981455096 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java @@ -0,0 +1,218 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread.strategy; + + +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.ExecutionStrategy; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ExecutionStrategy.Producer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ExecutionStrategyTest +{ + @Parameterized.Parameters(name = "{0}") + public static Iterable data() + { + return Arrays.asList(new Object[][]{ + {ProduceExecuteConsume.class}, + {ExecuteProduceConsume.class}, + {EatWhatYouKill.class} + }); + } + + QueuedThreadPool _threads = new QueuedThreadPool(20); + Class _strategyClass; + ExecutionStrategy _strategy; + + public ExecutionStrategyTest(Class strategy) + { + _strategyClass = strategy; + } + + void newExecutionStrategy(Producer producer, Executor executor) throws Exception + { + _strategy = _strategyClass.getConstructor(Producer.class,Executor.class).newInstance(producer,executor); + LifeCycle.start(_strategy); + } + + @Before + public void before() throws Exception + { + _threads.start(); + } + + @After + public void after() throws Exception + { + LifeCycle.stop(_strategy); + _threads.stop(); + } + + public static abstract class TestProducer implements Producer + { + @Override + public String toString() + { + return "TestProducer"; + } + } + + @Test + public void idleTest() throws Exception + { + AtomicInteger count = new AtomicInteger(0); + Producer producer = new TestProducer() + { + @Override + public Runnable produce() + { + count.incrementAndGet(); + return null; + } + }; + + newExecutionStrategy(producer,_threads); + _strategy.produce(); + assertThat(count.get(),greaterThan(0)); + } + + @Test + public void simpleTest() throws Exception + { + final int TASKS = 3*_threads.getMaxThreads(); + final CountDownLatch latch = new CountDownLatch(TASKS); + Producer producer = new TestProducer() + { + int tasks = TASKS; + @Override + public Runnable produce() + { + if (tasks-->0) + { + return new Runnable() + { + @Override + public void run() + { + latch.countDown(); + } + }; + } + + return null; + } + }; + + newExecutionStrategy(producer,_threads); + + for (int p=0; latch.getCount()>0 && p q = new ArrayBlockingQueue<>(500); + + Producer producer = new TestProducer() + { + int tasks=TASKS; + @Override + public Runnable produce() + { + if (tasks-->0) + { + try + { + final CountDownLatch latch = q.poll(10,TimeUnit.SECONDS); + + if (latch!=null) + { + return new Runnable() + { + @Override + public void run() + { + latch.countDown(); + } + }; + } + } + catch(InterruptedException e) + { + e.printStackTrace(); + } + } + + return null; + } + }; + + newExecutionStrategy(producer,_threads); + _threads.execute(()->_strategy.produce()); + + + + final CountDownLatch latch = new CountDownLatch(TASKS); + _threads.execute(new Runnable() + { + @Override + public void run() + { + try + { + for (int t=TASKS;t-->0;) + { + Thread.sleep(20); + q.offer(latch); + _strategy.produce(); + } + } + catch(Exception e) + { + e.printStackTrace(); + } + } + }); + + assertTrue(latch.await(10,TimeUnit.SECONDS)); + } +} diff --git a/jetty-util/src/test/resources/jetty-logging.properties b/jetty-util/src/test/resources/jetty-logging.properties index 76035ea1ee8..fe2f4676385 100644 --- a/jetty-util/src/test/resources/jetty-logging.properties +++ b/jetty-util/src/test/resources/jetty-logging.properties @@ -3,3 +3,5 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog #org.eclipse.jetty.util.LEVEL=DEBUG #org.eclipse.jetty.util.PathWatcher.Noisy.LEVEL=OFF + +org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.LEVEL=DEBUG \ No newline at end of file From d841dba997335f30c6d6e65229cb4d0b68998868 Mon Sep 17 00:00:00 2001 From: WalkerWatch Date: Mon, 3 Apr 2017 11:04:11 -0400 Subject: [PATCH 07/16] Documentation updates for #1437 Signed-off-by: WalkerWatch --- .../src/main/asciidoc/administration/logging/chapter.adoc | 2 +- .../logging/configuring-jetty-request-logs.adoc | 6 ++++++ .../logging/default-logging-with-stderrlog.adoc | 7 +++++++ jetty-util/src/main/config/modules/console-capture.mod | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/jetty-documentation/src/main/asciidoc/administration/logging/chapter.adoc b/jetty-documentation/src/main/asciidoc/administration/logging/chapter.adoc index b0f80f1a0ca..f8a6f664428 100644 --- a/jetty-documentation/src/main/asciidoc/administration/logging/chapter.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/logging/chapter.adoc @@ -19,7 +19,7 @@ This chapter discusses various options for configuring logging. -include::configuring-jetty-logging.adoc[] +//include::configuring-jetty-logging.adoc[] include::default-logging-with-stderrlog.adoc[] include::configuring-jetty-request-logs.adoc[] include::configuring-logging-modules.adoc[] diff --git a/jetty-documentation/src/main/asciidoc/administration/logging/configuring-jetty-request-logs.adoc b/jetty-documentation/src/main/asciidoc/administration/logging/configuring-jetty-request-logs.adoc index 3ec97cd8db6..3585f316b5d 100644 --- a/jetty-documentation/src/main/asciidoc/administration/logging/configuring-jetty-request-logs.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/logging/configuring-jetty-request-logs.adoc @@ -58,6 +58,12 @@ INFO: Base directory was modified The above command will add a new `requestlog.ini` file to your link:#start-vs-startd[`{$jetty.base}/start.d` directory]. +____ +[NOTE] +By default, request logs are not set to be appended, meaning a the log file is wiped clean upon sever restart. +You can change this setting by editing the `requestlog.ini` and un-commenting the line that reads `jetty.requestlog.append=true`. +____ + The equivalent code for embedded usages of Jetty is: [source, java, subs="{sub-order}"] diff --git a/jetty-documentation/src/main/asciidoc/administration/logging/default-logging-with-stderrlog.adoc b/jetty-documentation/src/main/asciidoc/administration/logging/default-logging-with-stderrlog.adoc index 6c430b727c9..0627dbae94a 100644 --- a/jetty-documentation/src/main/asciidoc/administration/logging/default-logging-with-stderrlog.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/logging/default-logging-with-stderrlog.adoc @@ -45,6 +45,13 @@ INFO : Base directory was modified The default configuration for logging output will create a file `${jetty.base}/logs/yyyy_mm_dd.stderrout.log` which allows configuration of the output directory by setting the `jetty.logs` property. +____ +[NOTE] +By default, logs are not set to be appended, meaning a the log file is wiped clean upon sever restart. +You can change this setting by editing the `console-capture.ini` and un-commenting the line that reads `jetty.console-capture.append=true`. +____ + + Just enabling the `console-capture` will simply output the values of STDERR and STDOUT to a log file. To customize the log further, a module named `logging-jetty` is available to provides a default properties file to configure. As with `console-capture`, you activate the `logging-jetty` on the command line. diff --git a/jetty-util/src/main/config/modules/console-capture.mod b/jetty-util/src/main/config/modules/console-capture.mod index fe3be0d17b1..b4cda2351e4 100644 --- a/jetty-util/src/main/config/modules/console-capture.mod +++ b/jetty-util/src/main/config/modules/console-capture.mod @@ -19,7 +19,7 @@ resources/ # jetty.console-capture.dir=logs ## Whether to append to existing file -# jetty.console-capture.append=false +# jetty.console-capture.append=true ## How many days to retain old log files # jetty.console-capture.retainDays=90 From 7547fc4b2951a8f370ab1f5c74680bee2908fd3d Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 3 Apr 2017 19:33:48 +0200 Subject: [PATCH 08/16] Fixed toConnectionString() to avoid NPE. --- .../src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index bf47ec551d3..7c004311387 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -307,12 +307,13 @@ public class SslConnection extends AbstractConnection b = _decryptedInput; int di=b==null?-1:b.remaining(); + Connection connection = _decryptedEndPoint.getConnection(); return String.format("%s@%x{%s,eio=%d/%d,di=%d}=>%s", getClass().getSimpleName(), hashCode(), _sslEngine.getHandshakeStatus(), ei,eo,di, - ((AbstractConnection)_decryptedEndPoint.getConnection()).toConnectionString()); + connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection); } public class DecryptedEndPoint extends AbstractEndPoint From 1d9f9e86c0db1a3aaa374434c30a711e54d445dc Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 3 Apr 2017 19:34:31 +0200 Subject: [PATCH 09/16] Added getter to expose nested callback to subclasses. --- .../src/main/java/org/eclipse/jetty/util/Callback.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java index f5992660609..b270ee510bc 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java @@ -119,6 +119,11 @@ public interface Callback extends Invocable this.callback = nested.callback; } + public Callback getCallback() + { + return callback; + } + @Override public void succeeded() { From a994244454e5b85e79e38e9e45ee32d88bb21ea5 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 3 Apr 2017 22:20:54 +0200 Subject: [PATCH 10/16] Code cleanup. --- .../org/eclipse/jetty/server/Response.java | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) 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 a0556a9d30d..2a1f84a412f 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 @@ -69,20 +69,11 @@ public class Response implements HttpServletResponse { private static final Logger LOG = Log.getLogger(Response.class); private static final String __COOKIE_DELIM="\",;\\ \t"; - private final static String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); - private final static int __MIN_BUFFER_SIZE = 1; - private final static HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES,DateGenerator.__01Jan1970); - - + private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); + private static final int __MIN_BUFFER_SIZE = 1; + private static final HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES,DateGenerator.__01Jan1970); // Cookie building buffer. Reduce garbage for cookie using applications - private static final ThreadLocal __cookieBuilder = new ThreadLocal() - { - @Override - protected StringBuilder initialValue() - { - return new StringBuilder(128); - } - }; + private static final ThreadLocal __cookieBuilder = ThreadLocal.withInitial(() -> new StringBuilder(128)); public enum OutputType { @@ -117,10 +108,9 @@ public class Response implements HttpServletResponse private ResponseWriter _writer; private long _contentLength = -1; - private enum EncodingFrom { NOT_SET, INFERRED, SET_LOCALE, SET_CONTENT_TYPE, SET_CHARACTER_ENCODING }; + private enum EncodingFrom { NOT_SET, INFERRED, SET_LOCALE, SET_CONTENT_TYPE, SET_CHARACTER_ENCODING } private static final EnumSet __localeOverride = EnumSet.of(EncodingFrom.NOT_SET,EncodingFrom.INFERRED); private static final EnumSet __explicitCharset = EnumSet.of(EncodingFrom.SET_LOCALE,EncodingFrom.SET_CHARACTER_ENCODING); - public Response(HttpChannel channel, HttpOutput out) { @@ -686,7 +676,7 @@ public class Response implements HttpServletResponse /** * Sends a response with one of the 300 series redirection codes. * @param code the redirect status code - * @param location the location to send in Location headers + * @param location the location to send in {@code Location} headers * @throws IOException if unable to send the redirect */ public void sendRedirect(int code, String location) throws IOException @@ -765,7 +755,7 @@ public class Response implements HttpServletResponse if (HttpHeader.CONTENT_LENGTH == name) { if (value == null) - _contentLength = -1l; + _contentLength = -1L; else _contentLength = Long.parseLong(value); } @@ -790,7 +780,7 @@ public class Response implements HttpServletResponse if (HttpHeader.CONTENT_LENGTH.is(name)) { if (value == null) - _contentLength = -1l; + _contentLength = -1L; else _contentLength = Long.parseLong(value); } @@ -800,8 +790,7 @@ public class Response implements HttpServletResponse @Override public Collection getHeaderNames() { - final HttpFields fields = _fields; - return fields.getFieldNamesCollection(); + return _fields.getFieldNamesCollection(); } @Override @@ -813,8 +802,7 @@ public class Response implements HttpServletResponse @Override public Collection getHeaders(String name) { - final HttpFields fields = _fields; - Collection i = fields.getValuesList(name); + Collection i = _fields.getValuesList(name); if (i == null) return Collections.emptyList(); return i; @@ -1290,7 +1278,7 @@ public class Response implements HttpServletResponse } if (preserveCookies) - cookies.forEach(f->_fields.add(f)); + cookies.forEach(_fields::add); else { Request request = getHttpChannel().getRequest(); From c285d6f8bbd839906e8c39d23db2f343be22c6ca Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 3 Apr 2017 22:25:22 +0200 Subject: [PATCH 11/16] Fixes #612 - Support HTTP Trailer. Added support on both the server and the client, for HTTP/1 and HTTP/2. --- .../org/eclipse/jetty/client/HttpRequest.java | 13 + .../eclipse/jetty/client/HttpResponse.java | 15 ++ .../org/eclipse/jetty/client/HttpSender.java | 74 ++++-- .../client/http/HttpReceiverOverHTTP.java | 12 +- .../jetty/client/http/HttpSenderOverHTTP.java | 147 ++++++++++-- .../fcgi/client/http/HttpSenderOverFCGI.java | 6 + .../org/eclipse/jetty/http/HttpGenerator.java | 6 +- .../client/http/HttpReceiverOverHTTP2.java | 42 ++-- .../client/http/HttpSenderOverHTTP2.java | 25 +- .../http2/server/HTTP2ServerConnection.java | 6 +- .../http2/server/HttpChannelOverHTTP2.java | 15 +- .../http2/server/HttpTransportOverHTTP2.java | 108 ++++++--- .../org/eclipse/jetty/server/Response.java | 14 +- .../jetty/server/HttpTrailersTest.java | 170 ------------- .../jetty/http/client/HttpTrailersTest.java | 227 ++++++++++++++++++ 15 files changed, 601 insertions(+), 279 deletions(-) delete mode 100644 jetty-server/src/test/java/org/eclipse/jetty/server/HttpTrailersTest.java create mode 100644 tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpTrailersTest.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index 445f1d3b1fb..d4eedb7840a 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import java.util.function.Supplier; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.ContentResponse; @@ -83,6 +84,7 @@ public class HttpRequest implements Request private Map attributes; private List requestListeners; private BiFunction pushListener; + private Supplier trailers; protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri) { @@ -589,6 +591,12 @@ public class HttpRequest implements Request return this; } + public HttpRequest trailers(Supplier trailers) + { + this.trailers = trailers; + return this; + } + @Override public ContentProvider getContent() { @@ -725,6 +733,11 @@ public class HttpRequest implements Request return pushListener; } + public Supplier getTrailers() + { + return trailers; + } + @Override public boolean abort(Throwable cause) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java index b2acae1f13f..f7e3945e915 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -23,6 +23,7 @@ import java.util.List; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpVersion; @@ -34,6 +35,7 @@ public class HttpResponse implements Response private HttpVersion version; private int status; private String reason; + private HttpFields trailers; public HttpResponse(Request request, List listeners) { @@ -97,6 +99,19 @@ public class HttpResponse implements Response return result; } + public HttpFields getTrailers() + { + return trailers; + } + + public HttpResponse trailer(HttpField trailer) + { + if (trailers == null) + trailers = new HttpFields(); + trailers.add(trailer); + return this; + } + @Override public boolean abort(Throwable cause) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java index 7d67bc5940e..bdae1570819 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java @@ -20,10 +20,12 @@ package org.eclipse.jetty.client; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.util.BufferUtil; @@ -31,7 +33,6 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Invocable.InvocationType; /** * {@link HttpSender} abstracts the algorithm to send HTTP requests, so that subclasses only implement @@ -42,9 +43,9 @@ import org.eclipse.jetty.util.thread.Invocable.InvocationType; * {@link HttpSender} governs two state machines. *

* The request state machine is updated by {@link HttpSender} as the various steps of sending a request - * are executed, see RequestState. + * are executed, see {@code RequestState}. * At any point in time, a user thread may abort the request, which may (if the request has not been - * completely sent yet) move the request state machine to RequestState#FAILURE. + * completely sent yet) move the request state machine to {@code RequestState#FAILURE}. * The request state machine guarantees that the request steps are executed (by I/O threads) only if * the request has not been failed already. *

@@ -64,7 +65,8 @@ public abstract class HttpSender implements AsyncContentProvider.Listener private final AtomicReference senderState = new AtomicReference<>(SenderState.IDLE); private final Callback commitCallback = new CommitCallback(); private final IteratingCallback contentCallback = new ContentCallback(); - private final Callback lastCallback = new LastContentCallback(); + private final Callback trailersCallback = new TrailersCallback(); + private final Callback lastCallback = new LastCallback(); private final HttpChannel channel; private HttpContent content; private Throwable failure; @@ -407,7 +409,7 @@ public abstract class HttpSender implements AsyncContentProvider.Listener /** * Implementations should send the content at the {@link HttpContent} cursor position over the wire. *

- * The {@link HttpContent} cursor is advanced by {@link HttpSender} at the right time, and if more + * The {@link HttpContent} cursor is advanced by HttpSender at the right time, and if more * content needs to be sent, this method is invoked again; subclasses need only to send the content * at the {@link HttpContent} cursor position. *

@@ -422,6 +424,15 @@ public abstract class HttpSender implements AsyncContentProvider.Listener */ protected abstract void sendContent(HttpExchange exchange, HttpContent content, Callback callback); + /** + * Implementations should send the HTTP trailers and notify the given {@code callback} of the + * result of this operation. + * + * @param exchange the exchange to send + * @param callback the callback to notify + */ + protected abstract void sendTrailers(HttpExchange exchange, Callback callback); + protected void reset() { HttpContent content = this.content; @@ -674,13 +685,6 @@ public abstract class HttpSender implements AsyncContentProvider.Listener private class CommitCallback implements Callback { - - @Override - public InvocationType getInvocationType() - { - return content.getInvocationType(); - } - @Override public void succeeded() { @@ -721,10 +725,20 @@ public abstract class HttpSender implements AsyncContentProvider.Listener if (content == null) return; - if (!content.hasContent()) + HttpRequest request = exchange.getRequest(); + Supplier trailers = request.getTrailers(); + boolean hasContent = content.hasContent(); + if (!hasContent) { - // No content to send, we are done. - someToSuccess(exchange); + if (trailers == null) + { + // No trailers or content to send, we are done. + someToSuccess(exchange); + } + else + { + sendTrailers(exchange, lastCallback); + } } else { @@ -825,7 +839,9 @@ public abstract class HttpSender implements AsyncContentProvider.Listener if (lastContent) { - sendContent(exchange, content, lastCallback); + HttpRequest request = exchange.getRequest(); + Supplier trailers = request.getTrailers(); + sendContent(exchange, content, trailers == null ? lastCallback : trailersCallback); return Action.IDLE; } @@ -884,19 +900,35 @@ public abstract class HttpSender implements AsyncContentProvider.Listener @Override protected void onCompleteSuccess() { - // Nothing to do, since we always return false from process(). - // Termination is obtained via LastContentCallback. + // Nothing to do, since we always return IDLE from process(). + // Termination is obtained via LastCallback. } } - private class LastContentCallback implements Callback + private class TrailersCallback implements Callback { @Override - public InvocationType getInvocationType() + public void succeeded() { - return content.getInvocationType(); + HttpExchange exchange = getHttpExchange(); + if (exchange == null) + return; + sendTrailers(exchange, lastCallback); } + @Override + public void failed(Throwable x) + { + HttpContent content = HttpSender.this.content; + if (content == null) + return; + content.failed(x); + anyToFailure(x); + } + } + + private class LastCallback implements Callback + { @Override public void succeeded() { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 3239f703b89..3c19684e930 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -276,7 +276,17 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res { return false; } - + + @Override + public void parsedTrailer(HttpField trailer) + { + HttpExchange exchange = getHttpExchange(); + if (exchange == null) + return; + + exchange.getResponse().trailer(trailer); + } + @Override public boolean messageComplete() { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java index d3976261831..a6b0b40cef0 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java @@ -23,26 +23,29 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpContent; import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpRequestException; import org.eclipse.jetty.client.HttpSender; import org.eclipse.jetty.client.api.ContentProvider; -import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; public class HttpSenderOverHTTP extends HttpSender { private final HttpGenerator generator = new HttpGenerator(); + private final HttpClient httpClient; private boolean shutdown; public HttpSenderOverHTTP(HttpChannelOverHTTP channel) { super(channel); + httpClient = channel.getHttpDestination().getHttpClient(); } @Override @@ -71,8 +74,7 @@ public class HttpSenderOverHTTP extends HttpSender { try { - HttpClient client = getHttpChannel().getHttpDestination().getHttpClient(); - ByteBufferPool bufferPool = client.getByteBufferPool(); + ByteBufferPool bufferPool = httpClient.getByteBufferPool(); ByteBuffer chunk = null; while (true) { @@ -90,6 +92,11 @@ public class HttpSenderOverHTTP extends HttpSender chunk = bufferPool.acquire(HttpGenerator.CHUNK_SIZE, false); break; } + case NEED_CHUNK_TRAILER: + { + callback.succeeded(); + return; + } case FLUSH: { EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint(); @@ -131,6 +138,21 @@ public class HttpSenderOverHTTP extends HttpSender } } + @Override + protected void sendTrailers(HttpExchange exchange, Callback callback) + { + try + { + new TrailersCallback(callback).iterate(); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug(x); + callback.failed(x); + } + } + @Override protected void reset() { @@ -181,7 +203,7 @@ public class HttpSenderOverHTTP extends HttpSender this.exchange = exchange; this.callback = callback; - Request request = exchange.getRequest(); + HttpRequest request = exchange.getRequest(); ContentProvider requestContent = request.getContent(); long contentLength = requestContent == null ? -1 : requestContent.getLength(); String path = request.getPath(); @@ -189,6 +211,7 @@ public class HttpSenderOverHTTP extends HttpSender if (query != null) path += "?" + query; metaData = new MetaData.Request(request.getMethod(), new HttpURI(path), request.getVersion(), request.getHeaders(), contentLength); + metaData.setTrailerSupplier(request.getTrailers()); if (!expects100Continue(request)) { @@ -201,9 +224,6 @@ public class HttpSenderOverHTTP extends HttpSender @Override protected Action process() throws Exception { - HttpClient client = getHttpChannel().getHttpDestination().getHttpClient(); - ByteBufferPool bufferPool = client.getByteBufferPool(); - while (true) { HttpGenerator.Result result = generator.generateRequest(metaData, headerBuffer, chunkBuffer, contentBuffer, lastContent); @@ -217,31 +237,28 @@ public class HttpSenderOverHTTP extends HttpSender { case NEED_HEADER: { - headerBuffer = bufferPool.acquire(client.getRequestBufferSize(), false); + headerBuffer = httpClient.getByteBufferPool().acquire(httpClient.getRequestBufferSize(), false); break; } case NEED_CHUNK: { - chunkBuffer = bufferPool.acquire(HttpGenerator.CHUNK_SIZE, false); + chunkBuffer = httpClient.getByteBufferPool().acquire(HttpGenerator.CHUNK_SIZE, false); break; } + case NEED_CHUNK_TRAILER: + { + return Action.SUCCEEDED; + } case FLUSH: { EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint(); + if (headerBuffer == null) + headerBuffer = BufferUtil.EMPTY_BUFFER; if (chunkBuffer == null) - { - if (contentBuffer == null) - endPoint.write(this, headerBuffer); - else - endPoint.write(this, headerBuffer, contentBuffer); - } - else - { - if (contentBuffer == null) - endPoint.write(this, headerBuffer, chunkBuffer); - else - endPoint.write(this, headerBuffer, chunkBuffer, contentBuffer); - } + chunkBuffer = BufferUtil.EMPTY_BUFFER; + if (contentBuffer == null) + contentBuffer = BufferUtil.EMPTY_BUFFER; + endPoint.write(this, headerBuffer, chunkBuffer, contentBuffer); generated = true; return Action.SCHEDULED; } @@ -296,13 +313,91 @@ public class HttpSenderOverHTTP extends HttpSender private void release() { - HttpClient client = getHttpChannel().getHttpDestination().getHttpClient(); - ByteBufferPool bufferPool = client.getByteBufferPool(); - bufferPool.release(headerBuffer); + ByteBufferPool bufferPool = httpClient.getByteBufferPool(); + if (headerBuffer != BufferUtil.EMPTY_BUFFER) + bufferPool.release(headerBuffer); headerBuffer = null; - if (chunkBuffer != null) + if (chunkBuffer != BufferUtil.EMPTY_BUFFER) bufferPool.release(chunkBuffer); chunkBuffer = null; + contentBuffer = null; + } + } + + private class TrailersCallback extends IteratingCallback + { + private final Callback callback; + private ByteBuffer chunkBuffer; + + public TrailersCallback(Callback callback) + { + this.callback = callback; + } + + @Override + protected Action process() throws Throwable + { + while (true) + { + HttpGenerator.Result result = generator.generateRequest(null, null, chunkBuffer, null, true); + if (LOG.isDebugEnabled()) + LOG.debug("Generated trailers {}/{}", result, generator); + switch (result) + { + case NEED_CHUNK_TRAILER: + { + chunkBuffer = httpClient.getByteBufferPool().acquire(httpClient.getRequestBufferSize(), false); + break; + } + case FLUSH: + { + EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint(); + endPoint.write(this, chunkBuffer); + return Action.SCHEDULED; + } + case SHUTDOWN_OUT: + { + shutdownOutput(); + return Action.SUCCEEDED; + } + case DONE: + { + return Action.SUCCEEDED; + } + default: + { + throw new IllegalStateException(result.toString()); + } + } + } + } + + @Override + public void succeeded() + { + release(); + super.succeeded(); + } + + @Override + public void failed(Throwable x) + { + release(); + callback.failed(x); + super.failed(x); + } + + @Override + protected void onCompleteSuccess() + { + super.onCompleteSuccess(); + callback.succeeded(); + } + + private void release() + { + httpClient.getByteBufferPool().release(chunkBuffer); + chunkBuffer = null; } } diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpSenderOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpSenderOverFCGI.java index 6598cc7a547..2877ae0ee61 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpSenderOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpSenderOverFCGI.java @@ -125,4 +125,10 @@ public class HttpSenderOverFCGI extends HttpSender getHttpChannel().flush(result); } } + + @Override + protected void sendTrailers(HttpExchange exchange, Callback callback) + { + callback.succeeded(); + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index 63994a335d1..e8ea548547a 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.http; -import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500; - import java.io.IOException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; @@ -34,6 +32,8 @@ import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500; + /** * HttpGenerator. Builds HTTP Messages. *

@@ -764,7 +764,7 @@ public class HttpGenerator } // Else if we are HTTP/1.1 and the content length is unknown and we are either persistent // or it is a request with content (which cannot EOF) or the app has requested chunking - else if (http11 && content_length<0 && (_persistent || assumed_content_request || chunked_hint)) + else if (http11 && (chunked_hint || content_length<0 && (_persistent || assumed_content_request))) { // we use chunking _endOfContent = EndOfContent.CHUNKED_CONTENT; diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java index 1bae533c97d..70913728500 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java @@ -71,27 +71,37 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen if (exchange == null) return; - HttpResponse response = exchange.getResponse(); - MetaData.Response metaData = (MetaData.Response)frame.getMetaData(); - response.version(metaData.getHttpVersion()).status(metaData.getStatus()).reason(metaData.getReason()); - - if (responseBegin(exchange)) + HttpResponse httpResponse = exchange.getResponse(); + MetaData metaData = frame.getMetaData(); + if (metaData.isResponse()) { - HttpFields headers = metaData.getFields(); - for (HttpField header : headers) - { - if (!responseHeader(exchange, header)) - return; - } + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + httpResponse.version(response.getHttpVersion()).status(response.getStatus()).reason(response.getReason()); - if (responseHeaders(exchange)) + if (responseBegin(exchange)) { - int status = metaData.getStatus(); - boolean informational = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101; - if (frame.isEndStream() || informational) - responseSuccess(exchange); + HttpFields headers = response.getFields(); + for (HttpField header : headers) + { + if (!responseHeader(exchange, header)) + return; + } + + if (responseHeaders(exchange)) + { + int status = response.getStatus(); + boolean informational = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101; + if (frame.isEndStream() || informational) + responseSuccess(exchange); + } } } + else + { + HttpFields trailers = metaData.getFields(); + trailers.forEach(httpResponse::trailer); + responseSuccess(exchange); + } } @Override diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java index 6092fe324cd..c2230e75cb9 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpSenderOverHTTP2.java @@ -19,11 +19,13 @@ package org.eclipse.jetty.http2.client.http; import java.net.URI; +import java.util.function.Supplier; import org.eclipse.jetty.client.HttpContent; import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpSender; -import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -49,11 +51,13 @@ public class HttpSenderOverHTTP2 extends HttpSender @Override protected void sendHeaders(HttpExchange exchange, final HttpContent content, final Callback callback) { - Request request = exchange.getRequest(); + HttpRequest request = exchange.getRequest(); String path = relativize(request.getPath()); HttpURI uri = new HttpURI(request.getScheme(), request.getHost(), request.getPort(), path, null, request.getQuery(), null); MetaData.Request metaData = new MetaData.Request(request.getMethod(), uri, HttpVersion.HTTP_2, request.getHeaders()); - HeadersFrame headersFrame = new HeadersFrame(metaData, null, !content.hasContent()); + Supplier trailers = request.getTrailers(); + metaData.setTrailerSupplier(trailers); + HeadersFrame headersFrame = new HeadersFrame(metaData, null, trailers == null && !content.hasContent()); HttpChannelOverHTTP2 channel = getHttpChannel(); Promise promise = new Promise() { @@ -66,7 +70,7 @@ public class HttpSenderOverHTTP2 extends HttpSender if (content.hasContent() && !expects100Continue(request)) { boolean advanced = content.advance(); - boolean lastContent = content.isLast(); + boolean lastContent = trailers == null && content.isLast(); if (advanced || lastContent) { DataFrame dataFrame = new DataFrame(stream.getId(), content.getByteBuffer(), lastContent); @@ -115,8 +119,19 @@ public class HttpSenderOverHTTP2 extends HttpSender else { Stream stream = getHttpChannel().getStream(); - DataFrame frame = new DataFrame(stream.getId(), content.getByteBuffer(), content.isLast()); + Supplier trailers = exchange.getRequest().getTrailers(); + DataFrame frame = new DataFrame(stream.getId(), content.getByteBuffer(), trailers == null && content.isLast()); stream.data(frame, callback); } } + + @Override + protected void sendTrailers(HttpExchange exchange, Callback callback) + { + Supplier trailers = exchange.getRequest().getTrailers(); + MetaData metaData = new MetaData(HttpVersion.HTTP_2, trailers.get()); + Stream stream = getHttpChannel().getStream(); + HeadersFrame trailersFrame = new HeadersFrame(stream.getId(), metaData, null, true); + stream.headers(trailersFrame, callback); + } } diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index c8036f48965..f1b1636b742 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -173,7 +173,11 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection LOG.debug("Processing trailers {} on {}", frame, stream); HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE); if (channel != null) - channel.onRequestTrailers(frame); + { + Runnable task = channel.onRequestTrailers(frame); + if (task != null) + offerTask(task, false); + } } public boolean onStreamTimeout(IStream stream, Throwable failure) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java index 21f463100e1..1a391090806 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java @@ -282,11 +282,12 @@ public class HttpChannelOverHTTP2 extends HttpChannel return handle || wasDelayed ? this : null; } - public void onRequestTrailers(HeadersFrame frame) + public Runnable onRequestTrailers(HeadersFrame frame) { HttpFields trailers = frame.getMetaData().getFields(); - onTrailers(trailers); - onRequestComplete(); + if (trailers.size() > 0) + onTrailers(trailers); + if (LOG.isDebugEnabled()) { Stream stream = getStream(); @@ -294,6 +295,14 @@ public class HttpChannelOverHTTP2 extends HttpChannel stream.getId(), Integer.toHexString(stream.getSession().hashCode()), System.lineSeparator(), trailers); } + + boolean handle = onRequestComplete(); + + boolean wasDelayed = _delayedUntilContent; + _delayedUntilContent = false; + if (wasDelayed) + _handled = true; + return handle || wasDelayed ? this : null; } public boolean isRequestHandled() diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 680cc93fc91..51eb2a90e26 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -20,7 +20,9 @@ package org.eclipse.jetty.http2.server; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -48,6 +50,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport private final Connector connector; private final HTTP2ServerConnection connection; private IStream stream; + private MetaData metaData; public HttpTransportOverHTTP2(Connector connector, HTTP2ServerConnection connection) { @@ -85,48 +88,60 @@ public class HttpTransportOverHTTP2 implements HttpTransport public void send(MetaData.Response info, boolean isHeadRequest, ByteBuffer content, boolean lastContent, Callback callback) { boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; - if (info != null) { + metaData = info; + int status = info.getStatus(); boolean informational = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101; - boolean committed = false; - if (!informational) - committed = commit.compareAndSet(false, true); - - if (committed || informational) + if (informational) { - if (hasContent) - { - Callback commitCallback = new Callback.Nested(callback) - { - @Override - public void succeeded() - { - if (transportCallback.start(callback, false)) - send(content, lastContent, transportCallback); - } - }; - if (transportCallback.start(commitCallback, true)) - commit(info, false, transportCallback); - } - else - { - if (transportCallback.start(callback, false)) - commit(info, lastContent, transportCallback); - } + if (transportCallback.start(callback, false)) + sendHeaders(info, false, transportCallback); } else { - callback.failed(new IllegalStateException("committed")); + boolean needsCommit = commit.compareAndSet(false, true); + if (needsCommit) + { + Supplier trailers = info.getTrailerSupplier(); + + if (hasContent) + { + Callback nested = trailers == null || !lastContent ? callback : new SendTrailers(callback); + Callback commitCallback = new Callback.Nested(nested) + { + @Override + public void succeeded() + { + if (transportCallback.start(nested, false)) + sendContent(content, lastContent, trailers == null && lastContent, transportCallback); + } + }; + if (transportCallback.start(commitCallback, true)) + sendHeaders(info, false, transportCallback); + } + else + { + Callback nested = trailers == null ? callback : new SendTrailers(callback); + if (transportCallback.start(nested, true)) + sendHeaders(info, trailers == null && lastContent, transportCallback); + } + } + else + { + callback.failed(new IllegalStateException("committed")); + } } } else { if (hasContent || lastContent) { - if (transportCallback.start(callback, false)) - send(content, lastContent, transportCallback); + Supplier trailers = metaData.getTrailerSupplier(); + Callback nested = trailers == null ? callback : new SendTrailers(callback); + if (transportCallback.start(nested, false)) + sendContent(content, lastContent, trailers == null && lastContent, transportCallback); } else { @@ -171,7 +186,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport }, new Stream.Listener.Adapter()); // TODO: handle reset from the client ? } - private void commit(MetaData.Response info, boolean endStream, Callback callback) + private void sendHeaders(MetaData.Response info, boolean endStream, Callback callback) { if (LOG.isDebugEnabled()) { @@ -185,7 +200,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport stream.headers(frame, callback); } - private void send(ByteBuffer content, boolean lastContent, Callback callback) + private void sendContent(ByteBuffer content, boolean lastContent, boolean endStream, Callback callback) { if (LOG.isDebugEnabled()) { @@ -193,10 +208,22 @@ public class HttpTransportOverHTTP2 implements HttpTransport stream.getId(), Integer.toHexString(stream.getSession().hashCode()), content.remaining(), lastContent ? " (last chunk)" : ""); } - DataFrame frame = new DataFrame(stream.getId(), content, lastContent); + DataFrame frame = new DataFrame(stream.getId(), content, endStream); stream.data(frame, callback); } + private void sendTrailers(MetaData metaData, Callback callback) + { + if (LOG.isDebugEnabled()) + { + LOG.debug("HTTP2 Response #{}/{}: trailers", + stream.getId(), Integer.toHexString(stream.getSession().hashCode())); + } + + HeadersFrame frame = new HeadersFrame(stream.getId(), metaData, null, true); + stream.headers(frame, callback); + } + public void onStreamFailure(Throwable failure) { transportCallback.failed(failure); @@ -277,7 +304,9 @@ public class HttpTransportOverHTTP2 implements HttpTransport } } if (LOG.isDebugEnabled()) - LOG.debug("HTTP2 Response #{} {}", stream.getId(), commit ? "committed" : "flushed content"); + LOG.debug("HTTP2 Response #{}/{} {}", + stream.getId(), Integer.toHexString(stream.getSession().hashCode()), + commit ? "committed" : "flushed content"); if (callback != null) callback.succeeded(); } @@ -340,4 +369,19 @@ public class HttpTransportOverHTTP2 implements HttpTransport { IDLE, WRITING, FAILED, TIMEOUT } + + private class SendTrailers extends Callback.Nested + { + private SendTrailers(Callback callback) + { + super(callback); + } + + @Override + public void succeeded() + { + if (transportCallback.start(getCallback(), false)) + sendTrailers(new MetaData(HttpVersion.HTTP_2, metaData.getTrailerSupplier().get()), transportCallback); + } + } } 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 2a1f84a412f..2bb188c27d8 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 @@ -27,6 +27,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.servlet.RequestDispatcher; @@ -107,6 +108,7 @@ public class Response implements HttpServletResponse private OutputType _outputType = OutputType.NONE; private ResponseWriter _writer; private long _contentLength = -1; + private Supplier trailers; private enum EncodingFrom { NOT_SET, INFERRED, SET_LOCALE, SET_CONTENT_TYPE, SET_CHARACTER_ENCODING } private static final EnumSet __localeOverride = EnumSet.of(EncodingFrom.NOT_SET,EncodingFrom.INFERRED); @@ -1308,10 +1310,20 @@ public class Response implements HttpServletResponse _out.resetBuffer(); } + public void setTrailers(Supplier trailers) + { + this.trailers = trailers; + } + + public Supplier getTrailers() + { + return trailers; + } + protected MetaData.Response newResponseMetaData() { MetaData.Response info = new MetaData.Response(_channel.getRequest().getHttpVersion(), getStatus(), getReason(), _fields, getLongContentLength()); - // TODO info.setTrailerSupplier(trailers); + info.setTrailerSupplier(getTrailers()); return info; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpTrailersTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpTrailersTest.java deleted file mode 100644 index 2994717c2fe..00000000000 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpTrailersTest.java +++ /dev/null @@ -1,170 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.server; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; - -public class HttpTrailersTest -{ - private Server server; - private ServerConnector connector; - - private void start(Handler handler) throws Exception - { - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - server.setHandler(handler); - server.start(); - } - - @After - public void dispose() throws Exception - { - if (server != null) - server.stop(); - } - - @Test - public void testServletRequestTrailers() throws Exception - { - String trailerName = "Trailer"; - String trailerValue = "value"; - start(new AbstractHandler.ErrorDispatchHandler() - { - @Override - protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - jettyRequest.setHandled(true); - - // Read the content first. - ServletInputStream input = jettyRequest.getInputStream(); - while (true) - { - int read = input.read(); - if (read < 0) - break; - } - - // Now the trailers can be accessed. - HttpFields trailers = jettyRequest.getTrailers(); - Assert.assertNotNull(trailers); - Assert.assertEquals(trailerValue, trailers.get(trailerName)); - } - }); - - try (Socket client = new Socket("localhost", connector.getLocalPort())) - { - client.setSoTimeout(5000); - - String request = "" + - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Transfer-Encoding: chunked\r\n" + - "\r\n" + - "0\r\n" + - trailerName + ": " + trailerValue + "\r\n" + - "\r\n"; - OutputStream output = client.getOutputStream(); - output.write(request.getBytes(StandardCharsets.UTF_8)); - output.flush(); - - HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream())); - Assert.assertNotNull(response); - Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); - } - } - - @Test - public void testHugeTrailer() throws Exception - { - start(new AbstractHandler.ErrorDispatchHandler() - { - @Override - protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - jettyRequest.setHandled(true); - - try - { - // EOF will not be reached because of the huge trailer. - ServletInputStream input = jettyRequest.getInputStream(); - while (true) - { - int read = input.read(); - if (read < 0) - break; - } - Assert.fail(); - } - catch (IOException x) - { - // Expected. - } - } - }); - - char[] huge = new char[1024 * 1024]; - Arrays.fill(huge, 'X'); - try (Socket client = new Socket("localhost", connector.getLocalPort())) - { - client.setSoTimeout(5000); - - try - { - String request = "" + - "GET / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Transfer-Encoding: chunked\r\n" + - "\r\n" + - "0\r\n" + - "Trailer: " + new String(huge) + "\r\n" + - "\r\n"; - OutputStream output = client.getOutputStream(); - output.write(request.getBytes(StandardCharsets.UTF_8)); - output.flush(); - - HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream())); - Assert.assertNotNull(response); - Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); - } - catch(Exception e) - { - // May be thrown if write fails and error handling is aborted - } - } - } -} diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpTrailersTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpTrailersTest.java new file mode 100644 index 00000000000..29e4ca5e3fb --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpTrailersTest.java @@ -0,0 +1,227 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.client; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.junit.Assert; +import org.junit.Test; + +public class HttpTrailersTest extends AbstractTest +{ + public HttpTrailersTest(Transport transport) + { + super(transport == Transport.FCGI ? null : transport); + } + + @Test + public void testRequestTrailersNoContent() throws Exception + { + testRequestTrailers(null); + } + + @Test + public void testRequestTrailersWithContent() throws Exception + { + testRequestTrailers("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)); + } + + private void testRequestTrailers(byte[] content) throws Exception + { + String trailerName = "Trailer"; + String trailerValue = "value"; + start(new AbstractHandler.ErrorDispatchHandler() + { + @Override + protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + + // Read the content first. + ServletInputStream input = jettyRequest.getInputStream(); + while (true) + { + int read = input.read(); + if (read < 0) + break; + } + + // Now the trailers can be accessed. + HttpFields trailers = jettyRequest.getTrailers(); + Assert.assertNotNull(trailers); + Assert.assertEquals(trailerValue, trailers.get(trailerName)); + } + }); + + HttpFields trailers = new HttpFields(); + trailers.put(trailerName, trailerValue); + + HttpRequest request = (HttpRequest)client.newRequest(newURI()); + request = request.trailers(() -> trailers); + if (content != null) + request.method(HttpMethod.POST).content(new BytesContentProvider(content)); + ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testEmptyRequestTrailers() throws Exception + { + start(new AbstractHandler.ErrorDispatchHandler() + { + @Override + protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + + // Read the content first. + ServletInputStream input = jettyRequest.getInputStream(); + while (true) + { + int read = input.read(); + if (read < 0) + break; + } + + // Now the trailers can be accessed. + HttpFields trailers = jettyRequest.getTrailers(); + Assert.assertNull(trailers); + } + }); + + HttpFields trailers = new HttpFields(); + HttpRequest request = (HttpRequest)client.newRequest(newURI()); + request = request.trailers(() -> trailers); + ContentResponse response = request.timeout(5, TimeUnit.SECONDS).send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testResponseTrailersNoContent() throws Exception + { + testResponseTrailers(null); + } + + @Test + public void testResponseTrailersWithContent() throws Exception + { + testResponseTrailers("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8)); + } + + private void testResponseTrailers(byte[] content) throws Exception + { + String trailerName = "Trailer"; + String trailerValue = "value"; + start(new AbstractHandler.ErrorDispatchHandler() + { + @Override + protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + + HttpFields trailers = new HttpFields(); + trailers.put(trailerName, trailerValue); + + Response jettyResponse = (Response)response; + jettyResponse.setTrailers(() -> trailers); + if (content != null) + response.getOutputStream().write(content); + } + }); + + AtomicReference failure = new AtomicReference<>(new Throwable("no_success")); + ContentResponse response = client.newRequest(newURI()) + .onResponseSuccess(r -> + { + try + { + HttpResponse httpResponse = (HttpResponse)r; + HttpFields trailers = httpResponse.getTrailers(); + Assert.assertNotNull(trailers); + Assert.assertEquals(trailerValue, trailers.get(trailerName)); + failure.set(null); + } + catch (Throwable x) + { + failure.set(x); + } + }) + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + Assert.assertNull(failure.get()); + } + + @Test + public void testEmptyResponseTrailers() throws Exception + { + start(new AbstractHandler.ErrorDispatchHandler() + { + @Override + protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + + HttpFields trailers = new HttpFields(); + + Response jettyResponse = (Response)response; + jettyResponse.setTrailers(() -> trailers); + } + }); + + AtomicReference failure = new AtomicReference<>(new Throwable("no_success")); + ContentResponse response = client.newRequest(newURI()) + .onResponseSuccess(r -> + { + try + { + HttpResponse httpResponse = (HttpResponse)r; + HttpFields trailers = httpResponse.getTrailers(); + Assert.assertNull(trailers); + failure.set(null); + } + catch (Throwable x) + { + failure.set(x); + } + }) + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + Assert.assertNull(failure.get()); + } +} From f53776628f15b22899026f05fed00b9610d06c8d Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 4 Apr 2017 09:08:44 -0700 Subject: [PATCH 12/16] Issue #1448 - Reduce unncessary URL creation --- .../java/org/eclipse/jetty/util/TypeUtil.java | 4 +- .../jetty/webapp/URLStreamHandlerUtil.java | 78 ++++++++++++ .../jetty/webapp/WebAppClassLoaderTest.java | 16 ++- .../WebAppClassLoaderUrlStreamTest.java | 113 ++++++++++++++++++ 4 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 jetty-webapp/src/test/java/org/eclipse/jetty/webapp/URLStreamHandlerUtil.java create mode 100644 jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderUrlStreamTest.java diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index 8897375a763..69f76c79c0c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -34,8 +34,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import javax.servlet.ServletContainerInitializer; - import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -723,7 +721,7 @@ public class TypeUtil { try { - return Resource.newResource(URIUtil.getJarSource(url.toString())); + return Resource.newResource(URIUtil.getJarSource(url.toURI())); } catch(Exception e) { diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/URLStreamHandlerUtil.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/URLStreamHandlerUtil.java new file mode 100644 index 00000000000..1960a494c45 --- /dev/null +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/URLStreamHandlerUtil.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.webapp; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLStreamHandlerFactory; +import java.util.Arrays; +import java.util.Optional; + +public final class URLStreamHandlerUtil +{ + public static void setFactory(URLStreamHandlerFactory factory) + { + try + { + // First, reset the factory field + Field factoryField = getURLStreamHandlerFactoryField(); + factoryField.setAccessible(true); + factoryField.set(null, null); + + if(factory != null) + { + // Next, set the factory + URL.setURLStreamHandlerFactory(factory); + } + } + catch(Throwable ignore) + { + ignore.printStackTrace(System.err); + } + } + + public static URLStreamHandlerFactory getFactory() + { + try + { + // First, reset the factory field + Field factoryField = getURLStreamHandlerFactoryField(); + factoryField.setAccessible(true); + return (URLStreamHandlerFactory) factoryField.get(null); + } + catch(Throwable ignore) + { + return null; + } + } + + private static Field getURLStreamHandlerFactoryField() + { + Optional optFactoryField = Arrays.stream(URL.class.getDeclaredFields()) + .filter((f) -> Modifier.isStatic(f.getModifiers()) && + f.getType().equals(URLStreamHandlerFactory.class)) + .findFirst(); + + if(optFactoryField.isPresent()) + return optFactoryField.get(); + + throw new RuntimeException( "Cannot find URLStreamHandlerFactory field in " + URL.class.getName() ); + } +} diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderTest.java index 8a1518a0086..2974ea9f3d0 100644 --- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderTest.java +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderTest.java @@ -18,15 +18,18 @@ package org.eclipse.jetty.webapp; -import static org.eclipse.jetty.toolchain.test.ExtraMatchers.*; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.eclipse.jetty.toolchain.test.ExtraMatchers.ordered; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeThat; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.net.URI; import java.net.URL; -import java.net.URLClassLoader; import java.nio.file.Path; import java.security.ProtectionDomain; import java.util.ArrayList; @@ -48,7 +51,7 @@ public class WebAppClassLoaderTest private Path testWebappDir; private WebAppContext _context; - private WebAppClassLoader _loader; + protected WebAppClassLoader _loader; @Before public void init() throws Exception @@ -241,6 +244,9 @@ public class WebAppClassLoaderTest @Test public void testResources() throws Exception { + // The existence of a URLStreamHandler changes the behavior + assumeThat("No URLStreamHandler in place", URLStreamHandlerUtil.getFactory(), nullValue()); + List expected = new ArrayList<>(); List resources; diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderUrlStreamTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderUrlStreamTest.java new file mode 100644 index 00000000000..296285052e6 --- /dev/null +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppClassLoaderUrlStreamTest.java @@ -0,0 +1,113 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.webapp; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; + +public class WebAppClassLoaderUrlStreamTest extends WebAppClassLoaderTest +{ + public static class URLHandlers implements URLStreamHandlerFactory + { + private static final String[] STREAM_HANDLER_PREFIXES; + + static + { + STREAM_HANDLER_PREFIXES = new String[]{ + "sun.net.www.protocol", + "org.apache.harmony.luni.internal.net.www.protocol", + "javax.net.ssl" + }; + } + + private Map handlers = new HashMap<>(); + private ClassLoader loader; + + public URLHandlers(ClassLoader loader) + { + this.loader = loader; + } + + private URLStreamHandler getBuiltInHandler(String protocol, ClassLoader classLoader) + { + URLStreamHandler handler = handlers.get(protocol); + + if (handler == null) + { + for (String prefix : STREAM_HANDLER_PREFIXES) + { + String className = prefix + '.' + protocol + ".Handler"; + try + { + Class clazz = Class.forName(className, false, classLoader); + handler = (URLStreamHandler) clazz.newInstance(); + break; + } + catch (Exception ignore) + { + ignore.printStackTrace(System.err); + } + } + + if (handler != null) + { + handlers.put(protocol, handler); + } + } + + if (handler == null) + { + throw new RuntimeException("Unable to find handler for protocol [" + protocol + "]"); + } + + return handler; + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) + { + try + { + return getBuiltInHandler(protocol, loader); + } + catch (Exception e) + { + throw new RuntimeException("Unable to create URLStreamHandler for protocol [" + protocol + "]"); + } + } + } + + @After + public void cleanupURLStreamHandlerFactory() + { + URLStreamHandlerUtil.setFactory(null); + } + + @Before + public void init() throws Exception + { + super.init(); + URLStreamHandlerUtil.setFactory(new URLHandlers(_loader)); + } +} From 9cdf8b046810e6c7c971b55254ccf8c030667f54 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 4 Apr 2017 09:18:37 -0700 Subject: [PATCH 13/16] Issue #1449 - Removing unused / broken __jvmLib reference --- .../eclipse/jetty/webapp/WebAppContext.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java index c408952b08f..f8deb94bcbb 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java @@ -61,7 +61,6 @@ import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.util.AttributesMap; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.MultiException; -import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; @@ -130,23 +129,6 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL "org.eclipse.jetty.servlets.PushSessionCacheFilter" //must be loaded by container classpath } ; - // Find the location of the JVM lib directory - public final static String __jvmlib; - static - { - String lib=null; - try - { - lib=TypeUtil.getLoadedFrom(System.class).getFile().getParentFile().toURI().toString(); - } - catch(Exception e) - { - LOG.warn(e); - lib=null; - } - __jvmlib=lib; - } - // Server classes are classes that are hidden from being // loaded by the web application using system classloader, // so if web application needs to load any of such classes, From f90976f635d8ef0081ec25c27e41aec2acf13437 Mon Sep 17 00:00:00 2001 From: WalkerWatch Date: Tue, 4 Apr 2017 16:25:03 -0400 Subject: [PATCH 14/16] Version reminder and misc cleanup. Signed-off-by: WalkerWatch --- .../configuring/security/authentication.adoc | 43 ++++++++++--------- .../getting-started/jetty-installing.adoc | 6 +++ .../introduction/jetty-coordinates.adoc | 8 +++- .../introduction/what-version.adoc | 6 +++ 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/jetty-documentation/src/main/asciidoc/configuring/security/authentication.adoc b/jetty-documentation/src/main/asciidoc/configuring/security/authentication.adoc index c58453c7ece..309c6e292d6 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/security/authentication.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/security/authentication.adoc @@ -27,9 +27,9 @@ Authorization:: ==== Configuring an Authentication mechanism -The jetty server supports several standard authentication mechanisms: http://en.wikipedia.org/wiki/Basic_access_authentication[BASIC]; http://en.wikipedia.org/wiki/Digest_authentication[DIGEST]; http://en.wikipedia.org/wiki/Form-based_authentication[FORM]; CLIENT-CERT; and other mechanisms can be plugged in using the extensible http://docs.oracle.com/cd/E19462-01/819-6717/gcszc/index.html[JASPI] or http://en.wikipedia.org/wiki/SPNEGO[SPNEGO] mechanisms. +Jetty server supports several standard authentication mechanisms: http://en.wikipedia.org/wiki/Basic_access_authentication[BASIC]; http://en.wikipedia.org/wiki/Digest_authentication[DIGEST]; http://en.wikipedia.org/wiki/Form-based_authentication[FORM]; CLIENT-CERT; and other mechanisms can be plugged in using the extensible http://docs.oracle.com/cd/E19462-01/819-6717/gcszc/index.html[JASPI] or http://en.wikipedia.org/wiki/SPNEGO[SPNEGO] mechanisms. -Internally, configuring an authentication mechanism is done by setting an instance of a the link:{JDURL}/org/eclipse/jetty/security/Authenticator.html[Authenticator] interface onto the link:{JDURL}/org/eclipse/jetty/security/SecurityHandler.html[SecurityHandler] of the context, but in most cases it is done by declaring a `< login-config>` element in the standard web.xml descriptor or via annotations. +Internally, configuring an authentication mechanism is done by setting an instance of a the link:{JDURL}/org/eclipse/jetty/security/Authenticator.html[Authenticator] interface onto the link:{JDURL}/org/eclipse/jetty/security/SecurityHandler.html[SecurityHandler] of the context, but in most cases it is done by declaring a `` element in the standard web.xml descriptor or via annotations. Below is an example taken from the link:{GITBROWSEURL}/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml?h=release-9[jetty-test-webapp web.xml] that configures BASIC authentication: @@ -103,7 +103,7 @@ When a request is received for a protected resource, the web container checks if The Servlet Specification does not address how the static security information in the `WEB-INF/web.xml` file is mapped to the runtime environment of the container. For Jetty, the link:{JDURL}/org/eclipse/jetty/security/LoginService.html[LoginService] performs this function. -A LoginService has a unique name, and gives access to information about a set of users. +A `LoginService` has a unique name, and gives access to information about a set of users. Each user has authentication information (e.g. a password) and a set of roles associated with him/herself. You may configure one or many different LoginServices depending on your needs. @@ -114,7 +114,8 @@ When a request to a web application requires authentication or authorization, Je ==== Scoping Security Realms -A LoginService has a unique name, and is composed of a set of users. Each user has authentication information (for example, a password) and a set of roles associated with him/herself. +A `LoginService` has a unique name, and is composed of a set of users. +Each user has authentication information (for example, a password) and a set of roles associated with him/herself. You can configure one or many different realms depending on your needs. * Configure a single LoginService to share common security information across all of your web applications. @@ -144,8 +145,8 @@ Here's an example of an xml file that defines an in-memory type of LoginService ---- -If you define more than one LoginService on a Server, you will need to specify which one you want used for each context. -You can do that by telling the context the name of the LoginService, or passing it the LoginService instance. +If you define more than one `LoginService` on a Server, you will need to specify which one you want used for each context. +You can do that by telling the context the name of the `LoginService`, or passing it the `LoginService` instance. Here's an example of doing both of these, using a link:#deployable-descriptor-file[context xml file]: [source, xml, subs="{sub-order}"] @@ -170,7 +171,7 @@ Here's an example of doing both of these, using a link:#deployable-descriptor-fi ===== Per-Webapp Scoped -Alternatively, you can define a LoginService for just a single web application. +Alternatively, you can define a `LoginService` for just a single web application. Here's how to define the same HashLoginService, but inside a link:#deployable-descriptor-file[context xml file]: [source, xml, subs="{sub-order}"] @@ -192,12 +193,12 @@ Here's how to define the same HashLoginService, but inside a link:#deployable-de ---- -Jetty provides a number of different LoginService types which can be seen in the next section. +Jetty provides a number of different `LoginService` types which can be seen in the next section. [[configuring-login-service]] ==== Configuring a LoginService -A link:{JDURL}/org/eclipse/jetty/security/LoginService.html[LoginService] instance is required by each context/webapp that has a authentication mechanism, which is used to check the validity of the username and credentials collected by the authentication mechanism. Jetty provides the following implementations of LoginService: +A link:{JDURL}/org/eclipse/jetty/security/LoginService.html[`LoginService`] instance is required by each context/webapp that has a authentication mechanism, which is used to check the validity of the username and credentials collected by the authentication mechanism. Jetty provides the following implementations of `LoginService`: link:{JDURL}/org/eclipse/jetty/security/HashLoginService.html[HashLoginService]:: A user realm that is backed by a hash map that is filled either programatically or from a Java properties file. @@ -211,16 +212,16 @@ link:{JDURL}/org/eclipse/jetty/jaas/JAASLoginService.html[JAASLoginService]:: link:{JDURL}/org/eclipse/jetty/security/SpnegoLoginService.html[SpnegoLoginService]:: http://en.wikipedia.org/wiki/SPNEGO[SPNEGO] Authentication; see the section on link:#spnego-support[SPNEGO support] for more information. -An instance of a LoginService can be matched to a context/webapp by: +An instance of a `LoginService` can be matched to a context/webapp by: -* A LoginService instance may be set directly on the SecurityHandler instance via embedded code or IoC XML -* Matching the realm-name defined in web.xml with the name of a LoginService instance that has been added to the Server instance as a dependent bean -* If only a single LoginService instance has been set on the Server then it is used as the login service for the context +* A `LoginService` instance may be set directly on the `SecurityHandler` instance via embedded code or IoC XML +* Matching the realm-name defined in web.xml with the name of a `LoginService` instance that has been added to the Server instance as a dependent bean +* If only a single `LoginService` instance has been set on the Server then it is used as the login service for the context [[hash-login-service]] ===== HashLoginService -The HashLoginService is a simple and efficient login service that loads usernames, credentials and roles from a Java properties file in the format: +The `HashLoginService` is a simple and efficient login service that loads usernames, credentials and roles from a Java properties file in the format: [source,properties] ---- @@ -249,7 +250,7 @@ guest: guest,read-only ---- -You configure the HashLoginService with a name and a reference to the location of the properties file: +You configure the `HashLoginService` with a name and a reference to the location of the properties file: [source, xml, subs="{sub-order}"] ---- @@ -378,12 +379,12 @@ When a user requests a resource that is access protected, the LoginService will Until Servlet 3.1, role-based authorization could define: -* access granted to a set of named roles -* access totally forbidden, regardless of role -* access granted to a user in any of the roles defined in the effective web.xml. -This is indicated by the special value of "*" for the `` of a ` `in the `` +* Access granted to a set of named roles +* Access totally forbidden, regardless of role +* Access granted to a user in any of the roles defined in the effective web.xml. +This is indicated by the special value of `*` for the `` of a `` in the `` With the advent of Servlet 3.1, there is now another authorization: -* access granted to any user who is authenticated, regardless of roles. -This is indicated by the special value of "**" for the `` of a `` in the `` +* Access granted to any user who is authenticated, regardless of roles. +This is indicated by the special value of `**` for the `` of a `` in the `` diff --git a/jetty-documentation/src/main/asciidoc/quick-start/getting-started/jetty-installing.adoc b/jetty-documentation/src/main/asciidoc/quick-start/getting-started/jetty-installing.adoc index e66716fb3a5..4069c53aec9 100644 --- a/jetty-documentation/src/main/asciidoc/quick-start/getting-started/jetty-installing.adoc +++ b/jetty-documentation/src/main/asciidoc/quick-start/getting-started/jetty-installing.adoc @@ -30,6 +30,12 @@ When you download and unpack the binary, it is extracted into a directory called Put this directory in a convenient location. The rest of the instructions in this documentation refer to this location as either `JETTY_HOME` or as `$(jetty.home).` +_____ +[IMPORTANT] +It is important that only stable releases are used in production environments. +Versions that have been deprecated or are released as Milestones (M) or Release Candidates (RC) are not suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. +_____ + [[distribution-content]] ==== Distribution Content diff --git a/jetty-documentation/src/main/asciidoc/quick-start/introduction/jetty-coordinates.adoc b/jetty-documentation/src/main/asciidoc/quick-start/introduction/jetty-coordinates.adoc index 5570f184cc3..8350bf18633 100644 --- a/jetty-documentation/src/main/asciidoc/quick-start/introduction/jetty-coordinates.adoc +++ b/jetty-documentation/src/main/asciidoc/quick-start/introduction/jetty-coordinates.adoc @@ -17,6 +17,12 @@ [[quickstart-jetty-coordinates]] === Finding Jetty in Maven +_____ +[IMPORTANT] +It is important that only stable releases are used in production environments. +Versions that have been deprecated or are released as Milestones (M) or Release Candidates (RC) are not suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. +_____ + ==== Maven Coordinates Jetty has existed in Maven Central almost since its inception, though the coordinates have changed over the years. @@ -34,7 +40,7 @@ The top level Project Object Model (POM) for the Jetty project is located under ---- -==== Changelogs in Central +==== Changelogs in Maven Central The changes between versions of Jetty are tracked in a file called VERSIONS.txt, which is under source control and is generated on release. Those generated files are also uploaded into Maven Central during the release of the top level POM. You can find them as a classifier marked artifact. diff --git a/jetty-documentation/src/main/asciidoc/quick-start/introduction/what-version.adoc b/jetty-documentation/src/main/asciidoc/quick-start/introduction/what-version.adoc index 93100663081..ed8672a4247 100644 --- a/jetty-documentation/src/main/asciidoc/quick-start/introduction/what-version.adoc +++ b/jetty-documentation/src/main/asciidoc/quick-start/introduction/what-version.adoc @@ -21,6 +21,12 @@ Jetty 9 is the most recent version of Jetty and has a great many improvements ov This documentation which focuses on Jetty 9. While many people continue to use older versions of Jetty, we generally recommend using Jetty 9 as it represents the version of Jetty that we will actively maintain and improve over the next few years. +_____ +[IMPORTANT] +It is important that only stable releases are used in production environments. +Versions that have been deprecated or are released as Milestones (M) or Release Candidates (RC) are not suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. +_____ + .Jetty Versions [width="100%",cols="12%,9%,15%,6%,21%,10%,6%,21%",options="header",] |======================================================================= From 7fc3dac5f340e6bc053240c084e82cdea4274bca Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 4 Apr 2017 14:41:10 +0200 Subject: [PATCH 15/16] Removed truststore.jks, not needed. --- .../client/AbstractHttpClientServerTest.java | 2 -- .../eclipse/jetty/client/HttpClientTLSTest.java | 2 -- .../client/TLSServerConnectionCloseTest.java | 2 -- jetty-client/src/test/resources/truststore.jks | Bin 916 -> 0 bytes 4 files changed, 6 deletions(-) delete mode 100644 jetty-client/src/test/resources/truststore.jks diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java index c6fdaffdacb..b591cd7481f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java @@ -71,8 +71,6 @@ public abstract class AbstractHttpClientServerTest sslContextFactory.setEndpointIdentificationAlgorithm(""); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks"); - sslContextFactory.setTrustStorePassword("storepwd"); } if (server == null) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 5b1e53a3402..70e78b54fab 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -72,8 +72,6 @@ public class HttpClientTLSTest sslContextFactory.setEndpointIdentificationAlgorithm(""); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks"); - sslContextFactory.setTrustStorePassword("storepwd"); return sslContextFactory; } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java index 929eb2faaf1..11437de7539 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java @@ -69,8 +69,6 @@ public class TLSServerConnectionCloseTest sslContextFactory.setEndpointIdentificationAlgorithm(""); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks"); - sslContextFactory.setTrustStorePassword("storepwd"); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); diff --git a/jetty-client/src/test/resources/truststore.jks b/jetty-client/src/test/resources/truststore.jks deleted file mode 100644 index 839cb8c35151c2b7c64afca24b6b72caad070a05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 916 zcmezO_TO6u1_mY|W(3o$xs} zE~X|%Muz1J{3AIFGbaABoD&*5saD@gH|APIn|qhRGl}gsUzm=o9G*UXZaLfkb^*)o zjA*-gTf)`m_MQJYE&gJ}p^PHkrj!4^W|XX5a=N7A{;n#yaON&k_bHloe-^*hm?Z91 zlB>xeD=<(C>yn{9D54u}krkl}HQ(Uscha(++qf!T9y+xaEfnXd1O zi0)T?voO%;QH9LK;*_O3mBblqm)!31vU@hm;^%>mh5U@y3R%l0gzi`2yxH!+?kPOi zt!Tnsz1x9B3U2~8STZp)GB6^C5HPs_Lx_=~O<3xi>MmQ;D_g$D<_pdct`+TyzWTQ= zW5Finm(sGEe;ty^>vg$!cV)t>;H#Mev23$*WWBpyJ}Ir;RW+Htrt6{Pk&qz&-XG2@ z8@{&Lu%DX7m47Uny+-3w`=4V611q#Ub(U`xZCtSK^2LO^3(s|HW&N14dV4@A&(kX% z*S_eUPs-bSWRp>avt;CP@7K+G&3=b&1eO-s3f`;Cf91p#$)FW&xME3L8sEBQQDVCvfG>mdwqnk+GXd2ihXqpv z;usF(WoYYmu8DZZa4%1z=+hI+*gpkUykAy5tj#grb*gH!M6TqIcifYBGVe^&T#-2O K*=+x>r_BKeJV|!| From e7ac447d63bfd4b9f7c15f63eeefd411c3f4e9c9 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 5 Apr 2017 15:27:43 +0200 Subject: [PATCH 16/16] Fixes #1452 - Add tests for [want|need]ClientAuth. --- .../client/ssl/NeedWantClientAuthTest.java | 233 ++++++++++++++++++ .../src/test/resources/client_keystore.jks | Bin 0 -> 3956 bytes 2 files changed, 233 insertions(+) create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java create mode 100644 jetty-client/src/test/resources/client_keystore.jks diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java new file mode 100644 index 00000000000..e849564df73 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java @@ -0,0 +1,233 @@ +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.ssl; + +import java.security.cert.Certificate; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; + +import org.eclipse.jetty.client.EmptyServerHandler; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +/** + * In order to work, client authentication needs a certificate + * signed by a CA that also signed the server certificate. + *

+ * For this test, the client certificate is signed with the server + * certificate, and the server certificate is self-signed. + */ +public class NeedWantClientAuthTest +{ + private Server server; + private ServerConnector connector; + private HttpClient client; + + private void startServer(SslContextFactory sslContextFactory, Handler handler) throws Exception + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + connector = new ServerConnector(server, sslContextFactory); + server.addConnector(connector); + + server.setHandler(handler); + server.start(); + } + + private void startClient(SslContextFactory sslContextFactory) throws Exception + { + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + client = new HttpClient(sslContextFactory); + client.setExecutor(clientThreads); + client.start(); + } + + private SslContextFactory createSslContextFactory() + { + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setEndpointIdentificationAlgorithm(""); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); + sslContextFactory.setKeyStorePassword("storepwd"); + return sslContextFactory; + } + + @After + public void dispose() throws Exception + { + if (client != null) + client.stop(); + if (server != null) + server.stop(); + } + + @Test + public void testWantClientAuthWithoutAuth() throws Exception + { + SslContextFactory serverSSL = new SslContextFactory(); + serverSSL.setKeyStorePath("src/test/resources/keystore.jks"); + serverSSL.setKeyStorePassword("storepwd"); + serverSSL.setWantClientAuth(true); + startServer(serverSSL, new EmptyServerHandler()); + + SslContextFactory clientSSL = new SslContextFactory(true); + startClient(clientSSL); + + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testWantClientAuthWithAuth() throws Exception + { + SslContextFactory serverSSL = new SslContextFactory(); + serverSSL.setKeyStorePath("src/test/resources/keystore.jks"); + serverSSL.setKeyStorePassword("storepwd"); + serverSSL.setWantClientAuth(true); + startServer(serverSSL, new EmptyServerHandler()); + CountDownLatch handshakeLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + try + { + SSLSession session = event.getSSLEngine().getSession(); + Certificate[] clientCerts = session.getPeerCertificates(); + Assert.assertNotNull(clientCerts); + Assert.assertThat(clientCerts.length, Matchers.greaterThan(0)); + handshakeLatch.countDown(); + } + catch (Throwable x) + { + x.printStackTrace(); + } + } + }); + + SslContextFactory clientSSL = new SslContextFactory(true); + clientSSL.setKeyStorePath("src/test/resources/client_keystore.jks"); + clientSSL.setKeyStorePassword("storepwd"); + startClient(clientSSL); + + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + Assert.assertTrue(handshakeLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testNeedClientAuthWithoutAuth() throws Exception + { + SslContextFactory serverSSL = new SslContextFactory(); + serverSSL.setKeyStorePath("src/test/resources/keystore.jks"); + serverSSL.setKeyStorePassword("storepwd"); + serverSSL.setNeedClientAuth(true); + startServer(serverSSL, new EmptyServerHandler()); + + SslContextFactory clientSSL = new SslContextFactory(true); + startClient(clientSSL); + CountDownLatch handshakeLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + Assert.assertThat(failure, Matchers.instanceOf(SSLHandshakeException.class)); + handshakeLatch.countDown(); + } + }); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(result -> + { + if (result.isFailed()) + latch.countDown(); + }); + + Assert.assertTrue(handshakeLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testNeedClientAuthWithAuth() throws Exception + { + SslContextFactory serverSSL = new SslContextFactory(); + serverSSL.setKeyStorePath("src/test/resources/keystore.jks"); + serverSSL.setKeyStorePassword("storepwd"); + serverSSL.setNeedClientAuth(true); + startServer(serverSSL, new EmptyServerHandler()); + CountDownLatch handshakeLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + try + { + SSLSession session = event.getSSLEngine().getSession(); + Certificate[] clientCerts = session.getPeerCertificates(); + Assert.assertNotNull(clientCerts); + Assert.assertThat(clientCerts.length, Matchers.greaterThan(0)); + handshakeLatch.countDown(); + } + catch (Throwable x) + { + x.printStackTrace(); + } + } + }); + + SslContextFactory clientSSL = new SslContextFactory(true); + clientSSL.setKeyStorePath("src/test/resources/client_keystore.jks"); + clientSSL.setKeyStorePassword("storepwd"); + startClient(clientSSL); + + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + Assert.assertTrue(handshakeLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/jetty-client/src/test/resources/client_keystore.jks b/jetty-client/src/test/resources/client_keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..9c31ff30c63b5efe401234d044db32cea9e157df GIT binary patch literal 3956 zcmeI#c{tSj9tZGQFxhF!l07O*G&9z5kSV*7QI@eC$ymlR!%#w!!7#QYOUO-G#!?ZY zl#pezH`W+JqQ*s(y>QiYPS1JnxzD}#uk+XW_0R`Ed`+czw3ObE~?}rI|zXO4CVqATR(kR~`B3uB*_dVl+LQDxk zuDNMlGuZ+EvKn6xl2907wPG~pTubr{@1 zgdYL_9>7$83;xolAp;NpaTq{$0GJHq2f)ZcE)W?A1kmy9&pU_rz z8p2YH<6*NRwK^*xx?FQ+W^ORYZma+@!OKzaAJuk8bCY|QGh7(hmtG`ga{JDzSI~ok z&Y;T^b)k}W;COZ8GzGnH`3v%?!e&8!N<2{}CWf+jiCx`f|Iv;z%P0&_u9ddnRfp{U zu}xd^lWFk7n)XZSTLEK**~#<>O9HtDNKlGa&}5kXf~~$D+!xuYQEjM-u|F#!&ST`H zeCUxu@AIg}oLt>leh~<0xDM-h{mA@XE5y6Y;*3CC;nTyy!0-ZQyqVr?w$^52++BX) zPyqX-6q^P)&XHJsh$%GJh?Fv}!n7hu&s$X>L0}*N_{RzD9hbcc0Ra@~p|vA*y6OYf z>&`^MThQ=>@s5MrSH1EoWm#epRa-B`2}uTw@sjg%=LYct&KxB!(BwmCws$oIv5YiqK)tVBIddwei5?_odV{P^9T1V!(S zw6a>vMV^^~xS1(p30_Ld)ce>)z*~Ym>GOM}^EZEO-j&j+a9`EBj?gNRz0v^z?0z9W?jb)q zm!Wq90AL5g$ZXqt<=|4{0I~rUL4tez0pCj-;G*CPdroFB&z%FHMA@%%*JF~|Vg% zdU`8MT&C|3wTt2swF_>>T>6SpHL$|s$!eyMz@?3W^!C>SsO6)>o7Ab~n1@puThXG#1lFOq z`}|tf)_RoXR~J?j`HXO+7c(Nz2xt0jSi3@) z-z16iiz~1y*6qRk^|~p@_4|RH8rf(DMgPdvY?47m!61X0Jed*w%DTzcBLuzF5@u4h z8G5X(fU!QX?uNWDcA23b}5@-`$3F>{gcJ)k!JmJ^pCy@{ACxh1}1RHKH zn7v9E^L5oPlYE#j`%IteB+gLl0=*Ba(Q_Amn z{`7RljUiF(Lx>W=Vgn5+m$c9{m6)5AQIM#zGTh_+$413CD%*{o)fcBpO^J7d6x<)u zL!vwn0#X*QX(MxGhSd`Jb8M>~4K>I!(PwrP7b8TVZ#J%a+^tV}D5Z<~dfqnLaTf{W z#n9rv*-03A;@VC$_#wr2(Iz7aca723X1XrwCda9$ykls!-G3PJg(-M-OmHIE-2wh@ zb@;4{Qb}33#Jm=^)t+yeO*7BXv+upmGU$9IrZnKEn^4ch?z&(%U6&MpaTq8akX&w~ zUE2C|TsEM?Ty}UV0}EHi2VSKt>&shRk__}&$c^8w$kWk!8LQo150ZK*{MKmWmTgv- ze}BX;;}0$})$=HN+v-Pd=eFmV^6l5D0{H^5R-^cN&FK+W)pY;kC#2Vi+V`)XGwRq; zC+o3f8fF4$Gua;;6}5_8I_)3NH8pvQyXn(BC4k-BLFJ)B?A7?s+N<5;>GZGlKBkU~ zBswLj-z1Qg${H(C?P4x?95;B=U)!Y|U@B_-r5>xtCMN$b96tcTi^OE^5FMS%5k>dM zFp&pmWw&TkcRyr(ednL0eCXqovQ;}|-fVg2{%xAKgzs!Xsb9;*l}2x$Y^DXa zBU(u8!?Mupov*om0{Uf{n0!y$krZw!?HUOJ&hHv7Xr%b+YF51{KRakqHhF{G#W*^q z$c&ErG&^0#-Y-{3-Na~3Mk(4nbImyTlv#DBX=!WU5EC7Or;RBrrSeSN6Kj^Fh}-X9 zEp&o+p5Cs%b@SDe2Hb|E=ng3x{kjh8G$G|~pdor;QTAqKYA?qRjQqrh;~qZj_V95M z#0hAH47e(YjMxA1Yr_2(<3n-}ADS>Vm>OIip$0=b2=gMoFA={jVFAC>h64(*A@~r8 z!GzzFeSb>i$b(t(C|uHOe-9_Fkp1nm%@6 zLs+pYUexDG);O<8WP_=%Cb23U_jLJELm`R8@XVLQcG8_?<1I#8gCo9hztDRUA!s~h z*kY2h`)DCc44CQCU5}sSV@o{XPPo`?gnMD3H3RM_xb`&p1Hzk>B#^x`^FttHy`miX z@G9x_2?OF(Uo&QS+v2EaEHiptYc1@~3n!-)KVIds%=|WIna`&hRVycNXRx5GLf-@x zx^$$BjgBe#4Bxs~7JvSm7Cvi&_kJF}w%u4G0ajJ$2|R9%sTUZS}#zvVAh;dWNmU zZnH&Y1<)Q1K0a|&%D^~=5;i7F?NE5rmrwi)i0@K; literal 0 HcmV?d00001