diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc index 45d1e360fa24..9f40bf915605 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc @@ -37,6 +37,8 @@ Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain Proxying is supported for any version of the HTTP protocol. +The communication between the client and the proxy may be encrypted, so that it would not be possible for another party on the same network as the client to know what servers the client connects to. + [[pg-client-http-proxy-socks5]] ===== SOCKS5 Proxy Support diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index 25a7e378fb99..51a72f93ebac 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -1386,14 +1386,14 @@ protected void onBeforeHandling(Request request) } @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { // Retrieve the before handling nanoTime. long beforeHandlingNanoTime = (long)request.getAttribute("beforeHandlingNanoTime"); - // Record the request processing time. + // Record the request processing time and the status that was sent back to the client. long processingTime = NanoTime.millisSince(beforeHandlingNanoTime); - System.getLogger("trackTime").log(INFO, "processing request %s took %d ms", request, processingTime); + System.getLogger("trackTime").log(INFO, "processing request %s took %d ms and ended with status code %d", request, processingTime, status); } } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java index f8561b18bcae..4d9171a23fad 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java @@ -434,32 +434,71 @@ public boolean remove(Connection connection) if (removed) { released(connection); - removed(connection); + onRemoved(connection); } return removed; } + /** + *

Callback method invoked when a new {@link Connection} has been created.

+ * + * @param connection the {@link Connection} that has been created + * @see #onRemoved(Connection) + */ protected void onCreated(Connection connection) { } + /** + * @param connection the {@link Connection} that become idle + * @param close whether this pool is closing + * @return {@code true} to indicate that the connection is idle, {@code false} otherwise + * @deprecated Racy API. Do not use. There is no replacement. + */ + @Deprecated(since = "12.0.8", forRemoval = true) protected boolean idle(Connection connection, boolean close) { return !close; } + /** + * @param connection the {@link Connection} that was acquired + * @deprecated Racy API. Do not use. There is no replacement. + */ + @Deprecated(since = "12.0.8", forRemoval = true) protected void acquired(Connection connection) { } + /** + * @param connection the {@link Connection} that was released + * @deprecated Racy API. Do not use. There is no replacement. + */ + @Deprecated(since = "12.0.8", forRemoval = true) protected void released(Connection connection) { } + /** + * @param connection the {@link Connection} that was removed + * @deprecated replaced by {@link #onRemoved(Connection)} + */ + @Deprecated(since = "12.0.8", forRemoval = true) protected void removed(Connection connection) { } + /** + *

Callback method invoked when a {@link Connection} has been removed from this pool.

+ * + * @param connection the {@link Connection} that was removed + * @see #onCreated(Connection) + */ + protected void onRemoved(Connection connection) + { + removed(connection); + } + Collection getIdleConnections() { return pool.stream() @@ -493,7 +532,7 @@ private void close(Pool.Entry entry) { if (LOG.isDebugEnabled()) LOG.debug("Removed terminated entry {}", entry); - removed(connection); + onRemoved(connection); IO.close(connection); } if (!entry.isInUse()) @@ -572,12 +611,11 @@ public void succeeded(Connection connection) pending.decrementAndGet(); reserved.enable(connection, false); idle(connection, false); - complete(null); + super.succeeded(connection); proceed(); } else { - // reduce pending on failure and if not multiplexing also reduce demand failed(new IllegalArgumentException("Invalid connection object: " + connection)); } } @@ -587,10 +625,9 @@ public void failed(Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Connection creation failed {}", reserved, x); - // reduce pending on failure and if not multiplexing also reduce demand pending.decrementAndGet(); reserved.remove(); - completeExceptionally(x); + super.failed(x); destination.failed(x); } } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java index 2845f1fbf929..e6d717f3aeb8 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java @@ -33,7 +33,7 @@ public abstract class AbstractConnectorHttpClientTransport extends AbstractHttpC protected AbstractConnectorHttpClientTransport(ClientConnector connector) { this.connector = Objects.requireNonNull(connector); - addBean(connector); + installBean(connector); } public ClientConnector getClientConnector() diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 100376aff51d..7edbf36e20b2 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -149,11 +149,11 @@ public HttpClient() public HttpClient(HttpClientTransport transport) { this.transport = Objects.requireNonNull(transport); - addBean(transport); + installBean(transport); this.connector = ((AbstractHttpClientTransport)transport).getContainedBeans(ClientConnector.class).stream().findFirst().orElseThrow(); - addBean(requestListeners); - addBean(handlers); - addBean(decoderFactories); + installBean(requestListeners); + installBean(handlers); + installBean(decoderFactories); } public HttpClientTransport getTransport() diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java index 830f6425ea33..8187a32e8738 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java @@ -38,35 +38,92 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + *

Client-side proxy configuration for HTTP proxying, as specified by + * RFC 9110.

+ *

By default the communication between client and proxy happens using + * the HTTP/1.1 protocol, but it may be configured to use + * also other HTTP protocol versions, such as HTTP/2.

+ */ public class HttpProxy extends ProxyConfiguration.Proxy { private static final Logger LOG = LoggerFactory.getLogger(HttpProxy.class); + /** + *

Creates a new instance with the given HTTP proxy host and port.

+ * + * @param host the HTTP proxy host name + * @param port the HTTP proxy port + */ public HttpProxy(String host, int port) { this(new Origin.Address(host, port), false); } + /** + *

Creates a new instance with the given HTTP proxy address.

+ *

When {@code secure=true} the communication between the client and the + * proxy will be encrypted (using this proxy {@link #getSslContextFactory()} + * which typically defaults to that of {@link HttpClient}.

+ * + * @param address the HTTP proxy address (host and port) + * @param secure whether the communication between the client and the HTTP proxy should be secure + */ public HttpProxy(Origin.Address address, boolean secure) { this(address, secure, new Origin.Protocol(List.of("http/1.1"), false)); } + /** + *

Creates a new instance with the given HTTP proxy address and protocol.

+ * + * @param address the HTTP proxy address (host and port) + * @param secure whether the communication between the client and the HTTP proxy should be secure + * @param protocol the protocol to use to communicate with the HTTP proxy + */ public HttpProxy(Origin.Address address, boolean secure, Origin.Protocol protocol) { this(new Origin(secure ? "https" : "http", address, null, protocol, Transport.TCP_IP), null); } + /** + *

Creates a new instance with the given HTTP proxy address and TLS configuration.

+ *

The {@link SslContextFactory} could have a different configuration from the + * one configured in {@link HttpClient}, and it is used to communicate with the HTTP + * proxy only (not to communicate with the servers).

+ * + * @param address the HTTP proxy address (host and port) + * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy + */ public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory) { this(address, sslContextFactory, new Origin.Protocol(List.of("http/1.1"), false)); } + /** + *

Creates a new instance with the given HTTP proxy address, TLS configuration and protocol.

+ *

The {@link SslContextFactory} could have a different configuration from the + * one configured in {@link HttpClient} and it is used to communicate with the HTTP + * proxy only (not to communicate with the servers).

+ * + * @param address the HTTP proxy address (host and port) + * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy + * @param protocol the protocol to use to communicate with the HTTP proxy + */ public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory, Origin.Protocol protocol) { this(new Origin(sslContextFactory == null ? "http" : "https", address, null, protocol, Transport.TCP_IP), sslContextFactory); } + /** + *

Creates a new instance with the given HTTP proxy {@link Origin} and TLS configuration.

+ *

The {@link SslContextFactory} could have a different configuration from the + * one configured in {@link HttpClient} and it is used to communicate with the HTTP + * proxy only (not to communicate with the servers).

+ * + * @param origin the HTTP proxy {@link Origin} information + * @param sslContextFactory the {@link SslContextFactory.Client} to use to communicate with the HTTP proxy + */ public HttpProxy(Origin origin, SslContextFactory.Client sslContextFactory) { super(origin, sslContextFactory); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java index 855631099ffb..2fb07066cdf6 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java @@ -33,13 +33,35 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + *

Client-side proxy configuration for SOCKS4, a de-facto standard.

+ *

Consider using SOCK5 instead, a protocol that has been standardized + * by IETF.

+ * + * @see Socks5Proxy + */ public class Socks4Proxy extends ProxyConfiguration.Proxy { + /** + *

Creates a new instance with the given SOCKS4 proxy host and port.

+ * + * @param host the SOCKS4 proxy host name + * @param port the SOCKS4 proxy port + */ public Socks4Proxy(String host, int port) { this(new Origin.Address(host, port), false); } + /** + *

Creates a new instance with the given SOCKS4 proxy address.

+ *

When {@code secure=true} the communication between the client and the + * proxy will be encrypted (using this proxy {@link #getSslContextFactory()} + * which typically defaults to that of {@link HttpClient}.

+ * + * @param address the SOCKS4 proxy address (host and port) + * @param secure whether the communication between the client and the SOCKS4 proxy should be secure + */ public Socks4Proxy(Origin.Address address, boolean secure) { super(address, secure, null, null); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java index f4e353ec3b08..56a800af7784 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java @@ -56,11 +56,26 @@ public class Socks5Proxy extends Proxy private final Map authentications = new LinkedHashMap<>(); + /** + *

Creates a new instance with the given SOCKS5 proxy host and port.

+ * + * @param host the SOCKS5 proxy host name + * @param port the SOCKS5 proxy port + */ public Socks5Proxy(String host, int port) { this(new Origin.Address(host, port), false); } + /** + *

Creates a new instance with the given SOCKS5 proxy address.

+ *

When {@code secure=true} the communication between the client and the + * proxy will be encrypted (using this proxy {@link #getSslContextFactory()} + * which typically defaults to that of {@link HttpClient}.

+ * + * @param address the SOCKS5 proxy address (host and port) + * @param secure whether the communication between the client and the SOCKS5 proxy should be secure + */ public Socks5Proxy(Origin.Address address, boolean secure) { super(address, secure, null, null); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java index 8f7da5848174..31f2d5fbcf3e 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java @@ -115,7 +115,7 @@ public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFac { super(connector); this.infos = infos.length == 0 ? List.of(HttpClientConnectionFactory.HTTP11) : List.of(infos); - this.infos.forEach(this::addBean); + this.infos.forEach(this::installBean); setConnectionPoolFactory(destination -> new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), 1) ); diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index f4ae856e9d7e..e03b46f293c4 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -494,7 +494,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } @@ -548,7 +548,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } diff --git a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java index 44278c50c3a2..01d395d5699c 100644 --- a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java +++ b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java @@ -90,7 +90,7 @@ protected ScanningAppProvider() protected ScanningAppProvider(FilenameFilter filter) { _filenameFilter = filter; - addBean(_appMap); + installBean(_appMap); } @Override diff --git a/jetty-core/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/DelegatingThreadPool.java b/jetty-core/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/DelegatingThreadPool.java index 37af842dad5c..7c3e202eea36 100644 --- a/jetty-core/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/DelegatingThreadPool.java +++ b/jetty-core/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/DelegatingThreadPool.java @@ -33,7 +33,7 @@ public DelegatingThreadPool(Executor executor) { _executor = executor; _tryExecutor = TryExecutor.asTryExecutor(executor); - addBean(_executor); + installBean(_executor); } public Executor getExecutor() diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCache.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCache.java index 5e6c34961b62..540023cd1c1c 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCache.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCache.java @@ -49,6 +49,12 @@ public CookieCache(CookieCompliance compliance) _parser = CookieParser.newParser(this, compliance, this); } + @Deprecated(forRemoval = true) + public CookieCache(CookieCompliance compliance, ComplianceViolation.Listener complianceListener) + { + this(compliance); + } + @Override public void onComplianceViolation(ComplianceViolation.Event event) { diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java index 99c645f7d3cb..88246cda0cf4 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java @@ -19,6 +19,7 @@ import java.util.TimeZone; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.thread.ThreadIdPool; /** * ThreadLocal Date formatters for HTTP style dates. @@ -37,14 +38,7 @@ public class DateGenerator static final String[] MONTHS = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"}; - private static final ThreadLocal __dateGenerator = new ThreadLocal() - { - @Override - protected DateGenerator initialValue() - { - return new DateGenerator(); - } - }; + private static final ThreadIdPool __dateGenerator = new ThreadIdPool<>(); public static final String __01Jan1970 = DateGenerator.formatDate(0); @@ -56,7 +50,7 @@ protected DateGenerator initialValue() */ public static String formatDate(long date) { - return __dateGenerator.get().doFormatDate(date); + return __dateGenerator.apply(DateGenerator::new, DateGenerator::doFormatDate, date); } /** diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java index a1e75007918a..2af820fb3ac1 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateParser.java @@ -18,6 +18,8 @@ import java.util.Locale; import java.util.TimeZone; +import org.eclipse.jetty.util.thread.ThreadIdPool; + /** * ThreadLocal data parsers for HTTP style dates */ @@ -47,17 +49,10 @@ public class DateParser public static long parseDate(String date) { - return DATE_PARSER.get().parse(date); + return DATE_PARSER.apply(DateParser::new, DateParser::parse, date); } - private static final ThreadLocal DATE_PARSER = new ThreadLocal() - { - @Override - protected DateParser initialValue() - { - return new DateParser(); - } - }; + private static final ThreadIdPool DATE_PARSER = new ThreadIdPool<>(); final SimpleDateFormat[] _dateReceive = new SimpleDateFormat[DATE_RECEIVE_FMT.length]; diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 867b33983cd1..f7907a83f765 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -542,6 +542,7 @@ private boolean quickStartRequestLine(ByteBuffer buffer) buffer.position(position + 2 * Long.BYTES); _methodString = HttpMethod.GET.asString(); _version = HttpVersion.HTTP_1_1; + _fieldCache.prepare(); setState(State.HEADER); _requestHandler.startRequest(_methodString, "/", _version); return true; @@ -551,6 +552,7 @@ private boolean quickStartRequestLine(ByteBuffer buffer) buffer.position(position + 2 * Long.BYTES); _methodString = HttpMethod.GET.asString(); _version = HttpVersion.HTTP_1_0; + _fieldCache.prepare(); setState(State.HEADER); _requestHandler.startRequest(_methodString, "/", _version); return true; diff --git a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java index 567f27db9392..2958bf141ced 100644 --- a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java @@ -43,7 +43,7 @@ public class ClientConnectionFactoryOverHTTP2 extends ContainerLifeCycle impleme public ClientConnectionFactoryOverHTTP2(HTTP2Client http2Client) { this.http2Client = http2Client; - addBean(http2Client); + installBean(http2Client); } @Override diff --git a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java index cd56a974030e..a5178b522418 100644 --- a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java @@ -53,7 +53,7 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport public HttpClientTransportOverHTTP2(HTTP2Client http2Client) { this.http2Client = http2Client; - addBean(http2Client); + installBean(http2Client); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); diff --git a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java index 77263270b914..b49687bac794 100644 --- a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java +++ b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java @@ -127,7 +127,7 @@ public HTTP2Client() public HTTP2Client(ClientConnector connector) { this.connector = connector; - addBean(connector); + installBean(connector); } public ClientConnector getClientConnector() diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java index 0b0a753d2495..43e24fea1183 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java @@ -126,8 +126,8 @@ public HTTP2Session(Scheduler scheduler, EndPoint endPoint, Parser parser, Gener this.recvWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE); this.writeThreshold = 32 * 1024; this.pushEnabled = true; // SPEC: by default, push is enabled. - addBean(flowControl); - addBean(flusher); + installBean(flowControl); + installBean(flusher); } @Override diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java index bcd57d9172ec..3b69140b4d76 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java @@ -92,9 +92,9 @@ protected AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration if (!isProtocolSupported(p)) throw new IllegalArgumentException("Unsupported HTTP2 Protocol variant: " + p); } - addBean(sessionContainer); + installBean(sessionContainer); this.httpConfiguration = Objects.requireNonNull(httpConfiguration); - addBean(httpConfiguration); + installBean(httpConfiguration); setInputBufferSize(Frame.DEFAULT_MAX_SIZE + Frame.HEADER_LENGTH); setUseInputDirectByteBuffers(httpConfiguration.isUseInputDirectByteBuffers()); setUseOutputDirectByteBuffers(httpConfiguration.isUseOutputDirectByteBuffers()); diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/DynamicTableTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/DynamicTableTest.java index a9d0830aa987..ea06bac6e362 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/DynamicTableTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/DynamicTableTest.java @@ -129,4 +129,57 @@ public void onHeaders(Stream stream, HeadersFrame frame) assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @ParameterizedTest + @CsvSource({"0,-1", "-1,0", "0,0"}) + public void testMaxTableCapacityZero(int clientMaxCapacity, int serverMaxCapacity) throws Exception + { + start(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + + if (clientMaxCapacity >= 0) + { + http2Client.setMaxDecoderTableCapacity(clientMaxCapacity); + http2Client.setMaxEncoderTableCapacity(clientMaxCapacity); + } + if (serverMaxCapacity >= 0) + { + connector.getConnectionFactory(AbstractHTTP2ServerConnectionFactory.class).setMaxEncoderTableCapacity(serverMaxCapacity); + connector.getConnectionFactory(AbstractHTTP2ServerConnectionFactory.class).setMaxDecoderTableCapacity(serverMaxCapacity); + } + + CountDownLatch serverPreface = new CountDownLatch(1); + Session session = newClientSession(new Session.Listener() + { + @Override + public void onSettings(Session session, SettingsFrame frame) + { + serverPreface.countDown(); + } + }); + assertTrue(serverPreface.await(5, TimeUnit.SECONDS)); + + MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, true); + CountDownLatch latch = new CountDownLatch(1); + session.newStream(frame, new Promise.Adapter<>(), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertEquals(200, response.getStatus()); + latch.countDown(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } } diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/MultiplexedConnectionPoolTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/MultiplexedConnectionPoolTest.java index c28aa61ad149..c0a383b1e10e 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/MultiplexedConnectionPoolTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/MultiplexedConnectionPoolTest.java @@ -121,7 +121,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } @@ -226,7 +226,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } @@ -301,7 +301,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } @@ -372,7 +372,7 @@ protected void onCreated(Connection connection) } @Override - protected void removed(Connection connection) + protected void onRemoved(Connection connection) { poolRemoveCounter.incrementAndGet(); } diff --git a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java index f6fa9a5a6dcd..a27ee788578f 100644 --- a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java @@ -35,7 +35,7 @@ public class ClientConnectionFactoryOverHTTP3 extends ContainerLifeCycle impleme public ClientConnectionFactoryOverHTTP3(HTTP3Client http3Client) { - addBean(http3Client); + installBean(http3Client); } @Override diff --git a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java index ef2b3bc65234..490ae2970f48 100644 --- a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java @@ -49,7 +49,7 @@ public class HttpClientTransportOverHTTP3 extends AbstractHttpClientTransport im public HttpClientTransportOverHTTP3(HTTP3Client http3Client) { this.http3Client = Objects.requireNonNull(http3Client); - addBean(http3Client); + installBean(http3Client); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); diff --git a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/ClientHTTP3Session.java b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/ClientHTTP3Session.java index 50c87abeeecb..34869280a893 100644 --- a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/ClientHTTP3Session.java +++ b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/ClientHTTP3Session.java @@ -58,7 +58,7 @@ public ClientHTTP3Session(HTTP3Configuration configuration, ClientQuicSession qu super(quicSession); this.configuration = configuration; session = new HTTP3SessionClient(this, listener, promise); - addBean(session); + installBean(session); session.setStreamIdleTimeout(configuration.getStreamIdleTimeout()); if (LOG.isDebugEnabled()) @@ -69,7 +69,7 @@ public ClientHTTP3Session(HTTP3Configuration configuration, ClientQuicSession qu InstructionFlusher encoderInstructionFlusher = new InstructionFlusher(quicSession, encoderEndPoint, EncoderStreamConnection.STREAM_TYPE); encoder = new QpackEncoder(new InstructionHandler(encoderInstructionFlusher)); encoder.setMaxHeadersSize(configuration.getMaxRequestHeadersSize()); - addBean(encoder); + installBean(encoder); if (LOG.isDebugEnabled()) LOG.debug("created encoder stream #{} on {}", encoderStreamId, encoderEndPoint); @@ -77,19 +77,19 @@ public ClientHTTP3Session(HTTP3Configuration configuration, ClientQuicSession qu QuicStreamEndPoint decoderEndPoint = openInstructionEndPoint(decoderStreamId); InstructionFlusher decoderInstructionFlusher = new InstructionFlusher(quicSession, decoderEndPoint, DecoderStreamConnection.STREAM_TYPE); decoder = new QpackDecoder(new InstructionHandler(decoderInstructionFlusher)); - addBean(decoder); + installBean(decoder); if (LOG.isDebugEnabled()) LOG.debug("created decoder stream #{} on {}", decoderStreamId, decoderEndPoint); long controlStreamId = newStreamId(StreamType.CLIENT_UNIDIRECTIONAL); QuicStreamEndPoint controlEndPoint = openControlEndPoint(controlStreamId); controlFlusher = new ControlFlusher(quicSession, controlEndPoint, true); - addBean(controlFlusher); + installBean(controlFlusher); if (LOG.isDebugEnabled()) LOG.debug("created control stream #{} on {}", controlStreamId, controlEndPoint); messageFlusher = new MessageFlusher(quicSession.getByteBufferPool(), encoder, configuration.isUseOutputDirectByteBuffers()); - addBean(messageFlusher); + installBean(messageFlusher); } public QpackDecoder getQpackDecoder() diff --git a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java index b8bfe7f32010..922c051ed3e4 100644 --- a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java +++ b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java @@ -146,7 +146,7 @@ public HTTP3Client(ClientQuicConfiguration quicConfiguration, ClientConnector co { this.quicConfiguration = quicConfiguration; this.connector = connector; - addBean(connector); + installBean(connector); connector.setSslContextFactory(quicConfiguration.getSslContextFactory()); // Allow the mandatory unidirectional streams, plus pushed streams. quicConfiguration.setMaxUnidirectionalRemoteStreams(48); diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3Session.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3Session.java index 4cd60a409829..0bf47b12e260 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3Session.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3Session.java @@ -57,7 +57,7 @@ public ServerHTTP3Session(HTTP3Configuration configuration, ServerQuicSession qu super(quicSession); this.configuration = configuration; session = new HTTP3SessionServer(this, listener); - addBean(session); + installBean(session); session.setStreamIdleTimeout(configuration.getStreamIdleTimeout()); if (LOG.isDebugEnabled()) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java index 5670d08dc603..1dd074703e25 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java @@ -16,6 +16,7 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.TypeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,7 +104,7 @@ private void tryExpandBufferCapacity(int remaining) if (remaining <= capacityLeft) return; int need = remaining - capacityLeft; - _currentSize = Math.min(_maxSize, ceilToNextPowerOfTwo(_currentSize + need)); + _currentSize = Math.min(_maxSize, TypeUtil.ceilToNextPowerOfTwo(_currentSize + need)); if (_retainableByteBuffer != null) { @@ -116,12 +117,6 @@ private void tryExpandBufferCapacity(int remaining) } } - private static int ceilToNextPowerOfTwo(int val) - { - int result = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(val - 1)); - return result > 0 ? result : Integer.MAX_VALUE; - } - /** * Takes the buffer out of the aggregator. Once the buffer has been taken out, * the aggregator resets itself and a new buffer will be acquired from the pool diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index 0d4a4bdf90da..b08dac5a5837 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -122,7 +122,7 @@ public ClientConnector() public ClientConnector(Configurator configurator) { this.configurator = Objects.requireNonNull(configurator); - addBean(configurator); + installBean(configurator); configurator.addBean(this, false); } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index 265e890f8bb6..c78d7bb7bf94 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -96,7 +96,7 @@ public ManagedSelector(SelectorManager selectorManager, int id) SelectorProducer producer = new SelectorProducer(); Executor executor = selectorManager.getExecutor(); _strategy = new AdaptiveExecutionStrategy(producer, executor); - addBean(_strategy, true); + installBean(_strategy, true); } public Selector getSelector() diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java index 0a6f96a06e54..fcd9049c990d 100644 --- a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java +++ b/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java @@ -136,7 +136,7 @@ public OpenIdConfiguration(@Name("issuer") String issuer, if (this.issuer == null) throw new IllegalArgumentException("Issuer was not configured"); - addBean(this.httpClient); + installBean(this.httpClient); } @Override diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java b/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java index dc783c3f8098..f4dcb0d61404 100644 --- a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java +++ b/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java @@ -58,8 +58,8 @@ public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginS { this.configuration = Objects.requireNonNull(configuration); this.loginService = loginService; - addBean(this.configuration); - addBean(this.loginService); + installBean(this.configuration); + installBean(this.loginService); setAuthenticateNewUsers(configuration.isAuthenticateNewUsers()); } diff --git a/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java b/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java index 7aa2fa7b7c47..282de78f31db 100644 --- a/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java +++ b/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java @@ -20,7 +20,6 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/ProtocolSession.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/ProtocolSession.java index 7eb2fd1a48b2..452fdcbacc02 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/ProtocolSession.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/ProtocolSession.java @@ -48,7 +48,7 @@ public ProtocolSession(QuicSession session) { this.session = session; this.strategy = new AdaptiveExecutionStrategy(producer, session.getExecutor()); - addBean(strategy); + installBean(strategy); } public QuicSession getQuicSession() diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java index 664522383b34..c853fd1aec86 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java @@ -82,7 +82,7 @@ protected QuicSession(Executor executor, Scheduler scheduler, ByteBufferPool byt this.quicheConnection = quicheConnection; this.connection = connection; this.flusher = new Flusher(scheduler); - addBean(flusher); + installBean(flusher); this.remoteAddress = remoteAddress; Arrays.setAll(ids, i -> new AtomicLong()); } diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java index 7b90dafc1405..bfb4fb6d2ee4 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java @@ -89,7 +89,10 @@ interface quiche_error QUICHE_ERR_OUT_OF_IDENTIFIERS = -18, // Error in key update. - QUICHE_ERR_KEY_UPDATE = -19; + QUICHE_ERR_KEY_UPDATE = -19, + + // The peer sent more data in CRYPTO frames than we can buffer. + QUICHE_ERR_CRYPTO_BUFFER_EXCEEDED = -20; static String errToString(long err) { @@ -131,6 +134,8 @@ static String errToString(long err) return "QUICHE_ERR_OUT_OF_IDENTIFIERS"; if (err == QUICHE_ERR_KEY_UPDATE) return "QUICHE_ERR_KEY_UPDATE"; + if (err == QUICHE_ERR_CRYPTO_BUFFER_EXCEEDED) + return "QUICHE_ERR_CRYPTO_BUFFER_EXCEEDED"; return "?? " + err; } } diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java index 9c1990690c4d..e7f9ead641c0 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java @@ -33,7 +33,7 @@ public class quiche_h { // This interface is a translation of the quiche.h header of a specific version. // It needs to be reviewed each time the native lib version changes. - private static final String EXPECTED_QUICHE_VERSION = "0.20.0"; + private static final String EXPECTED_QUICHE_VERSION = "0.20.1"; public static final byte C_FALSE = 0; public static final byte C_TRUE = 1; diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java index 2c321f0e8624..9b6f7ba3392f 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java @@ -31,7 +31,7 @@ public interface LibQuiche extends Library { // This interface is a translation of the quiche.h header of a specific version. // It needs to be reviewed each time the native lib version changes. - String EXPECTED_QUICHE_VERSION = "0.20.0"; + String EXPECTED_QUICHE_VERSION = "0.20.1"; // The charset used to convert java.lang.String to char * and vice versa. Charset CHARSET = StandardCharsets.UTF_8; diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java index 82648394030a..72660597b355 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java @@ -60,7 +60,7 @@ public RewriteHandler(Handler handler, RuleContainer rules) { super(handler); _rules = rules; - addBean(_rules); + installBean(_rules); } /** diff --git a/jetty-core/jetty-security/pom.xml b/jetty-core/jetty-security/pom.xml index 1d9e19052ed5..bc725cb5ef56 100644 --- a/jetty-core/jetty-security/pom.xml +++ b/jetty-core/jetty-security/pom.xml @@ -12,7 +12,7 @@ The common Jetty security implementation - 2.1.5 + 2.1.6 2.0.0.AM27 ${project.groupId}.security org.eclipse.jetty.security.* diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java index af5fb04a3a5d..a04def5f1386 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java @@ -40,7 +40,7 @@ public abstract class AbstractLoginService extends ContainerLifeCycle implements protected AbstractLoginService() { - addBean(_identityService); + installBean(_identityService); } @Override diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SPNEGOLoginService.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SPNEGOLoginService.java index ee45735ba79c..ec95a5fdd9a7 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SPNEGOLoginService.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SPNEGOLoginService.java @@ -67,7 +67,7 @@ public SPNEGOLoginService(String realm, LoginService loginService) { _realm = realm; _loginService = loginService; - addBean(_loginService); + installBean(_loginService); } /** diff --git a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index 6d16baf2bda7..eba634c5bed7 100644 --- a/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-core/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -98,7 +98,7 @@ protected SecurityHandler() protected SecurityHandler(Handler handler) { super(handler); - addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories)); + installBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories)); } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index b50095800557..5be4f03d2a35 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -181,13 +181,13 @@ public AbstractConnector( _server = Objects.requireNonNull(server); _executor = executor != null ? executor : _server.getThreadPool(); - addBean(_executor, executor != null); + installBean(_executor, executor != null); _scheduler = scheduler != null ? scheduler : _server.getScheduler(); - addBean(_scheduler, scheduler != null); + installBean(_scheduler, scheduler != null); _bufferPool = bufferPool != null ? bufferPool : server.getByteBufferPool(); - addBean(_bufferPool, bufferPool != null); + installBean(_bufferPool, bufferPool != null); for (ConnectionFactory factory : factories) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java index da8c27853471..50d00d0ba77d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AllowedResourceAliasChecker.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -158,6 +159,11 @@ protected boolean check(String pathInContext, Path path) protected boolean check(String pathInContext, Resource resource) { + // If there is a single Path available, check it + Path path = resource.getPath(); + if (path != null && Files.exists(path)) + return check(pathInContext, path); + // Allow any aliases (symlinks, 8.3, casing, etc.) so long as // the resulting real file is allowed. for (Resource r : resource) @@ -186,7 +192,7 @@ protected boolean isAllowed(Path path) for (String protectedTarget : _protected) { Resource p = _baseResource.resolve(protectedTarget); - if (p == null) + if (Resources.missing(p)) continue; for (Resource r : p) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java index c3c04f910a12..2c9f3e716c67 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java @@ -336,7 +336,7 @@ public CustomRequestLog(RequestLog.Writer writer, String formatString) { _formatString = formatString; _requestLogWriter = writer; - addBean(_requestLogWriter); + installBean(_requestLogWriter); try { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java index befd3afe2f36..c68a69e6324c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/DetectorConnectionFactory.java @@ -49,7 +49,7 @@ public DetectorConnectionFactory(Detecting... detectingConnectionFactories) _detectingConnectionFactories = Arrays.asList(detectingConnectionFactories); for (Detecting detectingConnectionFactory : detectingConnectionFactories) { - addBean(detectingConnectionFactory); + installBean(detectingConnectionFactory); } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java index 691a741f67ca..3344f065d25c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java @@ -355,7 +355,7 @@ default Singleton getTail() } /** - *

Utility method to perform sanity checks before adding the given {@code Handler} to + *

Utility method to perform sanity checks before updating the given {@code Handler} to * the given {@code Singleton}, typically used in implementations of {@link #setHandler(Handler)}.

*

The sanity checks are:

*
    @@ -364,15 +364,41 @@ default Singleton getTail() *
  • Sets the {@code Server} on the {@code Handler}
  • *
  • Update the beans on the {@code Singleton} if it is a {@link ContainerLifeCycle}
  • *
+ * @param singleton the {@code Singleton} to set the {@code Handler} + * @param handler the {@code Handler} to set + * @see #checkHandler(Singleton, Handler) + * @return The {@code Handler} to set + */ + static Handler updateHandler(Singleton singleton, Handler handler) + { + // check state + checkHandler(singleton, handler); + + if (singleton instanceof org.eclipse.jetty.util.component.ContainerLifeCycle container) + container.updateBean(singleton.getHandler(), handler); + + return handler; + } + + /** + *

Utility method to perform sanity checks on a {{@link Handler} to be added to + * the given {@code Singleton}.

+ *

The sanity checks are:

+ *
    + *
  • Check for the server start state and whether the invocation type is compatible
  • + *
  • Check for {@code Handler} loops
  • + *
  • Sets the {@code Server} on the {@code Handler}
  • + *
  • Update the beans on the {@code Singleton} if it is a {@link ContainerLifeCycle}
  • + *
* - * @param wrapper the {@code Singleton} to set the {@code Handler} + * @param singleton the {@code Singleton} to set the {@code Handler} * @param handler the {@code Handler} to set * @return The {@code Handler} to set */ - static Handler updateHandler(Singleton wrapper, Handler handler) + static Handler checkHandler(Singleton singleton, Handler handler) { // check state - Server server = wrapper.getServer(); + Server server = singleton.getServer(); // If the collection is changed whilst started, then the risk is that if we switch from NON_BLOCKING to BLOCKING // whilst the execution strategy may have already dispatched the very last available thread, thinking it would @@ -386,16 +412,13 @@ static Handler updateHandler(Singleton wrapper, Handler handler) } // Check for loops. - if (handler == wrapper || (handler instanceof Handler.Container container && - container.getDescendants().contains(wrapper))) + if (handler == singleton || (handler instanceof Handler.Container container && + container.getDescendants().contains(singleton))) throw new IllegalStateException("Handler loop"); if (handler != null && server != null) handler.setServer(server); - if (wrapper instanceof org.eclipse.jetty.util.component.ContainerLifeCycle container) - container.updateBean(wrapper.getHandler(), handler); - return handler; } } @@ -692,7 +715,8 @@ public Wrapper(Handler handler) public Wrapper(boolean dynamic, Handler handler) { super(dynamic); - _handler = handler == null ? null : Singleton.updateHandler(this, handler); + _handler = handler == null ? null : Singleton.checkHandler(this, handler); + installBean(_handler); } @Override diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java index 82b373364c15..30b5d9b5067a 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java @@ -46,7 +46,7 @@ public HttpConnectionFactory(@Name("config") HttpConfiguration config) { super(HttpVersion.HTTP_1_1.asString()); _config = Objects.requireNonNull(config); - addBean(_config); + installBean(_config); setUseInputDirectByteBuffers(_config.isUseInputDirectByteBuffers()); setUseOutputDirectByteBuffers(_config.isUseOutputDirectByteBuffers()); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java index d65301fbf588..52d0d74f0aad 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/LowResourceMonitor.java @@ -267,7 +267,7 @@ public void setLowResourceChecks(Set lowResourceChecks) public void addLowResourceCheck(LowResourceCheck lowResourceCheck) { - addBean(lowResourceCheck); + installBean(lowResourceCheck); this._lowResourceChecks.add(lowResourceCheck); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index bed6d6fbcffa..855d0a2be91d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -115,7 +115,7 @@ public Server(@Name("port") int port) ServerConnector connector = new ServerConnector(this); connector.setPort(port); addConnector(connector); - addBean(_attributes); + installBean(_attributes); } /** @@ -142,13 +142,13 @@ public Server(@Name("threadPool") ThreadPool pool) public Server(@Name("threadPool") ThreadPool threadPool, @Name("scheduler") Scheduler scheduler, @Name("bufferPool") ByteBufferPool bufferPool) { _threadPool = threadPool != null ? threadPool : new QueuedThreadPool(); - addBean(_threadPool); + installBean(_threadPool); _scheduler = scheduler != null ? scheduler : new ScheduledExecutorScheduler(); - addBean(_scheduler); + installBean(_scheduler); _bufferPool = bufferPool != null ? bufferPool : new ArrayByteBufferPool(); - addBean(_bufferPool); + installBean(_bufferPool); setServer(this); - addBean(FileSystemPool.INSTANCE, false); + installBean(FileSystemPool.INSTANCE, false); } public Handler getDefaultHandler() diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java index d5b8832e2b34..838c8af60620 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java @@ -211,7 +211,7 @@ public ServerConnector( { super(server, executor, scheduler, bufferPool, acceptors, factories); _manager = newSelectorManager(getExecutor(), getScheduler(), selectors); - addBean(_manager, true); + installBean(_manager, true); setAcceptorPriorityDelta(-2); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java index aa894af0c3a5..e757fdd1b25f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java @@ -56,7 +56,7 @@ public SslConnectionFactory(@Name("sslContextFactory") SslContextFactory.Server super("SSL"); _sslContextFactory = factory == null ? new SslContextFactory.Server() : factory; _nextProtocol = nextProtocol; - addBean(_sslContextFactory); + installBean(_sslContextFactory); } public SslContextFactory.Server getSslContextFactory() diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java index 6460a0e766fb..f629dc41beb3 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SymlinkAllowedResourceAliasChecker.java @@ -67,10 +67,11 @@ protected boolean check(String pathInContext, Path path) // Add the segment to the path and realURI. segmentPath.append("/").append(segment); Resource fromBase = _baseResource.resolve(segmentPath.toString()); - for (Resource r : fromBase) - { - Path p = r.getPath(); + // If there is a single path, check it + Path p = fromBase.getPath(); + if (p != null) + { // If the ancestor of the alias is a symlink, then check if the real URI is protected, otherwise allow. // This allows symlinks like /other->/WEB-INF and /external->/var/lib/docroot // This does not allow symlinks like /WeB-InF->/var/lib/other @@ -80,10 +81,28 @@ protected boolean check(String pathInContext, Path path) // If the ancestor is not allowed then do not allow. if (!isAllowed(p)) return false; + } + else + { + // otherwise check all possibles + for (Resource r : fromBase) + { + p = r.getPath(); + + // If the ancestor of the alias is a symlink, then check if the real URI is protected, otherwise allow. + // This allows symlinks like /other->/WEB-INF and /external->/var/lib/docroot + // This does not allow symlinks like /WeB-InF->/var/lib/other + if (Files.isSymbolicLink(p)) + return !getContextHandler().isProtectedTarget(segmentPath.toString()); - // TODO as we are building the realURI of the resource, it would be possible to - // re-check that against security constraints. + // If the ancestor is not allowed then do not allow. + if (!isAllowed(p)) + return false; + } } + + // TODO as we are building the realURI of the resource, it would be possible to + // re-check that against security constraints. } } catch (Throwable t) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 629afb82b597..c53a65f6f919 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -388,8 +388,6 @@ public void removeVirtualHosts(String... virtualHosts) @ManagedAttribute(value = "Virtual hosts accepted by the context", readonly = true) public List getVirtualHosts() { - if (_vhosts == null) - return null; return _vhosts.stream().map(VHost::getName).collect(Collectors.toList()); } @@ -1044,7 +1042,7 @@ public String toString() b.append(",b=").append(getBaseResource()); b.append(",a=").append(_availability); - if (vhosts != null && !vhosts.isEmpty()) + if (!vhosts.isEmpty()) { b.append(",vh=["); b.append(String.join(",", vhosts)); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index caad1fb06392..896eea30e7ca 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -74,6 +74,7 @@ public class ErrorHandler implements Request.Handler boolean _showStacks = true; boolean _showMessageInTitle = true; + String _defaultResponseMimeType = Type.TEXT_HTML.asString(); HttpField _cacheControl = new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); public ErrorHandler() @@ -127,7 +128,7 @@ protected void generateResponse(Request request, Response response, int code, St callback.succeeded(); return; } - acceptable = Collections.singletonList(Type.TEXT_HTML.asString()); + acceptable = Collections.singletonList(_defaultResponseMimeType); } List charsets = request.getHeaders().getQualityCSV(HttpHeader.ACCEPT_CHARSET).stream() .map(s -> @@ -477,6 +478,23 @@ public void setShowMessageInTitle(boolean showMessageInTitle) _showMessageInTitle = showMessageInTitle; } + /** + * @return The mime type to be used when a client does not specify an Accept header, or the request did not fully parse + */ + @ManagedAttribute("Mime type to be used when a client does not specify an Accept header, or the request did not fully parse") + public String getDefaultResponseMimeType() + { + return _defaultResponseMimeType; + } + + /** + * @param defaultResponseMimeType The mime type to be used when a client does not specify an Accept header, or the request did not fully parse + */ + public void setDefaultResponseMimeType(String defaultResponseMimeType) + { + _defaultResponseMimeType = Objects.requireNonNull(defaultResponseMimeType);; + } + protected void write(Writer writer, String string) throws IOException { if (string == null) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java index 4ff0547a4ea7..b338f5cb3bb6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EventsHandler.java @@ -75,7 +75,7 @@ public boolean handle(Request request, Response response, Callback callback) thr { notifyOnResponseBegin(roRequest, wrappedResponse); notifyOnResponseTrailersComplete(roRequest, wrappedResponse); - notifyOnComplete(roRequest, x); + notifyOnComplete(roRequest, wrappedResponse, x); }); boolean handled = super.handle(wrappedRequest, wrappedResponse, callback); @@ -179,11 +179,11 @@ private void notifyOnResponseTrailersComplete(Request request, EventsResponse re } } - private void notifyOnComplete(Request request, Throwable failure) + private void notifyOnComplete(Request request, Response response, Throwable failure) { try { - onComplete(request, failure); + onComplete(request, response.getStatus(), response.getHeaders().asImmutable(), failure); } catch (Throwable x) { @@ -308,12 +308,31 @@ protected void onResponseTrailersComplete(Request request, HttpFields trailers) * has been completed). * * @param request the request object. The {@code read()}, {@code demand(Runnable)} and {@code fail(Throwable)} methods must not be called by the listener. - * @param failure if there was a failure to complete + * @param failure if there was a failure to complete. + * @deprecated Override {@link #onComplete(Request, int, HttpFields, Throwable)} instead. */ + @Deprecated protected void onComplete(Request request, Throwable failure) + { + } + + /** + * Invoked when the request and response processing are complete, + * just before the request and response will be recycled (i.e. after the + * {@link Runnable} return from {@link org.eclipse.jetty.server.HttpChannel#onRequest(MetaData.Request)} + * has returned and the {@link Callback} passed to {@link Handler#handle(Request, Response, Callback)} + * has been completed). + * + * @param request the request object. The {@code read()}, {@code demand(Runnable)} and {@code fail(Throwable)} methods must not be called by the listener. + * @param status the response status. + * @param headers the immutable fields of the response object. + * @param failure if there was a failure to complete. + */ + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { if (LOG.isDebugEnabled()) - LOG.debug("onComplete of {}", request, failure); + LOG.debug("onComplete of {} status={} headers={}", request, status, headers); + onComplete(request, failure); } private class EventsResponse extends Response.Wrapper diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/LatencyRecordingHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/LatencyRecordingHandler.java index 908bcb65188d..d3f11adc0ac7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/LatencyRecordingHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/LatencyRecordingHandler.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.server.handler; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.NanoTime; @@ -35,7 +36,7 @@ public LatencyRecordingHandler(Handler handler) } @Override - protected final void onComplete(Request request, Throwable failure) + protected final void onComplete(Request request, int status, HttpFields headers, Throwable failure) { onRequestComplete(request.getId(), NanoTime.since(request.getBeginNanoTime())); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java index 13bd9622d174..4d6573e4b09e 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java @@ -89,19 +89,6 @@ protected void onRequestRead(Request request, Content.Chunk chunk) _bytesRead.add(chunk.remaining()); } - @Override - protected void onResponseBegin(Request request, int status, HttpFields headers) - { - switch (status / 100) - { - case 1 -> _responses1xx.increment(); - case 2 -> _responses2xx.increment(); - case 3 -> _responses3xx.increment(); - case 4 -> _responses4xx.increment(); - case 5 -> _responses5xx.increment(); - } - } - @Override protected void onResponseWrite(Request request, boolean last, ByteBuffer content) { @@ -111,12 +98,20 @@ protected void onResponseWrite(Request request, boolean last, ByteBuffer content } @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { if (failure != null) _failures.increment(); _requestTimeStats.record(NanoTime.since(request.getBeginNanoTime())); _requestStats.decrement(); + switch (status / 100) + { + case 1 -> _responses1xx.increment(); + case 2 -> _responses2xx.increment(); + case 3 -> _responses3xx.increment(); + case 4 -> _responses4xx.increment(); + case 5 -> _responses5xx.increment(); + } } @Override diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java index 386aa48962f8..52eea5824262 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.logging.StacklessLogging; @@ -159,6 +160,31 @@ public void test404NoAccept() throws Exception assertContent(response); } + @Test + public void test404NoAcceptButSpecifiedDefaultResponseMimeType() throws Exception + { + ErrorHandler errorHandler = new ErrorHandler(); + errorHandler.setDefaultResponseMimeType(MimeTypes.Type.APPLICATION_JSON.asString()); + server.setErrorHandler(errorHandler); + + String rawResponse = connector.getResponse(""" + GET / HTTP/1.1 + Host: Localhost + + """); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status code", response.getStatus(), is(404)); + assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("application/json")); + assertThat(response.get(HttpHeader.DATE), notNullValue()); + assertThat(response.getContent(), containsString("\"status\":\"404\"")); + assertThat(response.getContent(), containsString("\"message\":\"Not Found\"")); + + assertContent(response); + } + @Test public void test404EmptyAccept() throws Exception { diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java index 7238184b05f9..f61ef2818385 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java @@ -13,14 +13,21 @@ package org.eclipse.jetty.server.handler; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.NanoTime; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -29,6 +36,7 @@ import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; @@ -84,7 +92,7 @@ protected void onResponseBegin(Request request, int status, HttpFields headers) } @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { attribute.set((String)request.getAttribute(ATTRIBUTE_NAME)); } @@ -112,7 +120,7 @@ public void testNanoTimestamps() throws Exception EventsHandler eventsHandler = new EventsHandler(new EchoHandler()) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { beginNanoTime.set(request.getBeginNanoTime()); readyNanoTime.set(request.getHeadersNanoTime()); @@ -144,4 +152,82 @@ protected void onComplete(Request request, Throwable failure) assertThat(NanoTime.millisSince(readyNanoTime.get()), greaterThan(450L)); } } + + @Test + public void testEventsOfNoopHandler() throws Exception + { + List events = new CopyOnWriteArrayList<>(); + + EventsHandler eventsHandler = new EventsHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }) + { + @Override + protected void onRequestRead(Request request, Content.Chunk chunk) + { + events.add("onRequestRead"); + } + + @Override + protected void onResponseWrite(Request request, boolean last, ByteBuffer content) + { + events.add("onResponseWrite"); + } + + @Override + protected void onResponseWriteComplete(Request request, Throwable failure) + { + events.add("onResponseWriteComplete"); + } + + @Override + protected void onResponseTrailersComplete(Request request, HttpFields trailers) + { + events.add("onResponseTrailersComplete"); + } + + @Override + protected void onBeforeHandling(Request request) + { + events.add("onBeforeHandling"); + } + + @Override + protected void onAfterHandling(Request request, boolean handled, Throwable failure) + { + events.add("onAfterHandling"); + } + + @Override + protected void onResponseBegin(Request request, int status, HttpFields headers) + { + events.add("onResponseBegin"); + } + + @Override + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) + { + events.add("onComplete"); + } + }; + + startServer(eventsHandler); + + String rawRequest = """ + GET / HTTP/1.1\r + Host: localhost\r + Connection: close\r + \r + """; + + String response = connector.getResponse(rawRequest); + assertThat(response, containsString("HTTP/1.1 200 OK")); + assertThat(events, equalTo(Arrays.asList("onBeforeHandling", "onAfterHandling", "onResponseBegin", "onComplete"))); + } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java index 1a1a2a477562..8bcd7d440a02 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; @@ -47,6 +48,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -307,6 +309,87 @@ public void testTwoRequestsSerially() throws Exception assertEquals(0, _statsHandler.getFailures()); } + @Test + public void testIncompleteContentRecordsStatus200() throws Exception + { + _statsHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + }); + _server.start(); + + String request = """ + POST / HTTP/1.1 + Host: localhost + Transfer-Encoding: chunked + Connection: close + + 0a + 0123456789 + """; + + String response = _connector.getResponse(request); + assertThat(response, containsString("HTTP/1.1 200 OK")); + assertThat(response, containsString("Connection: close")); + await().atMost(5, TimeUnit.SECONDS).until(_statsHandler::getResponses2xx, is(1)); + } + + @Test + public void testInvalidContentLengthRecordsStatus500() throws Exception + { + _statsHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + response.getHeaders().put("content-length", 1L); + callback.succeeded(); + return true; + } + }); + _server.start(); + + String request = """ + GET / HTTP/1.1\r + Host: localhost\r + \r + """; + + String response = _connector.getResponse(request); + assertThat(response, is(nullValue())); + await().atMost(5, TimeUnit.SECONDS).until(_statsHandler::getResponses5xx, is(1)); + } + + @Test + public void testImplicitStatus200Recorded() throws Exception + { + _statsHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Do not explicitly set status to 200. + response.write(true, ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), callback); + return true; + } + }); + _server.start(); + + String request = """ + GET / HTTP/1.1\r + Host: localhost\r + \r + """; + String response = _connector.getResponse(request); + assertThat(response, containsString("HTTP/1.1 200 OK")); + await().atMost(5, TimeUnit.SECONDS).until(_statsHandler::getResponses2xx, is(1)); + } + @Test public void testTwoRequestsInParallel() throws Exception { diff --git a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/CachingSessionDataStore.java b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/CachingSessionDataStore.java index 7941f25efb17..5e9c6e98965b 100644 --- a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/CachingSessionDataStore.java +++ b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/CachingSessionDataStore.java @@ -52,14 +52,14 @@ public class CachingSessionDataStore extends ContainerLifeCycle implements Sessi /** * @param cache the front cache to use - * @param store the actual store for the the session data + * @param store the actual store for the session data */ public CachingSessionDataStore(SessionDataMap cache, SessionDataStore store) { _cache = cache; - addBean(_cache, true); + installBean(_cache, true); _store = store; - addBean(_store, true); + installBean(_store, true); } /** diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java index 026fa5702e80..6fa6d887243e 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java @@ -372,7 +372,7 @@ protected void onResponseWriteComplete(Request request, Throwable failure) } @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { addEvent("onComplete"); } @@ -441,7 +441,7 @@ protected void onResponseTrailersComplete(Request request, HttpFields trailers) } @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { // System.out.println("onComplete"); useForbiddenMethods(request, exceptions); diff --git a/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java b/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java index 0629b66e4fba..5681314f03c3 100644 --- a/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java +++ b/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java @@ -82,7 +82,7 @@ public UnixDomainServerConnector(Server server, Executor executor, Scheduler sch { super(server, executor, scheduler, bufferPool, acceptors, factories.length > 0 ? factories : new ConnectionFactory[]{new HttpConnectionFactory()}); selectorManager = newSelectorManager(getExecutor(), getScheduler(), selectors); - addBean(selectorManager, true); + installBean(selectorManager, true); } protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentPool.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentPool.java index 22cae03b0d2c..c96a75f6c236 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentPool.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ConcurrentPool.java @@ -265,7 +265,7 @@ private int startIndex(int size) case FIRST -> 0; case RANDOM -> ThreadLocalRandom.current().nextInt(size); case ROUND_ROBIN -> nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size; - case THREAD_ID -> (int)((Thread.currentThread().getId() * 31) % size); + case THREAD_ID -> (int)(Thread.currentThread().getId() % size); }; } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java index d65aa3d37529..d98fdb82ea74 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java @@ -81,6 +81,8 @@ private static String formatTimestamp(String timestamp) { try { + if (StringUtil.isBlank(timestamp)) + return "unknown"; long epochMillis = Long.parseLong(timestamp); return Instant.ofEpochMilli(epochMillis).toString(); } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java index 4b53a983c7f5..09c12e0dc931 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java @@ -52,4 +52,11 @@ public static int getLongsPerCacheLine() { return getCacheLineBytes() >> 3; } + + public static int getReferencesPerCacheLine() + { + // Use getIntegersPerCacheLine() instead of getLongsPerCacheLine() b/c refs (oops) could be + // compressed down to 32-bit; maybe this could be detected? + return getIntegersPerCacheLine(); + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java index 4409d113e120..66134183b066 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java @@ -371,7 +371,7 @@ public Scanner(Scheduler scheduler, boolean reportRealPaths) { //Create the scheduler and start it _scheduler = scheduler == null ? new ScheduledExecutorScheduler("Scanner-" + SCANNER_IDS.getAndIncrement(), true, 1) : scheduler; - addBean(_scheduler); + installBean(_scheduler); _linkOptions = reportRealPaths ? new LinkOption[0] : new LinkOption[] {LinkOption.NOFOLLOW_LINKS}; } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index a2ae71be17ce..8b9510164d7c 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -803,4 +803,17 @@ private TypeUtil() { // prevents instantiation } + + /** + * Get the next highest power of two + * @param value An integer + * @return a power of two that is greater than or equal to {@code value} + */ + public static int ceilToNextPowerOfTwo(int value) + { + if (value < 0) + throw new IllegalArgumentException("value must not be negative"); + int result = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); + return result > 0 ? result : Integer.MAX_VALUE; + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java index 2b8f639b271a..0e4a0ae37bcd 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java @@ -300,6 +300,7 @@ public boolean isUnmanaged(Object bean) * The {@link #addBean(Object, boolean)} * method should be used if this is not correct, or the {@link #manage(Object)} and {@link #unmanage(Object)} * methods may be used after an add to change the status. + *

This method should not be called from a constructor, instead use {@link #installBean(Object)}

* * @param o the bean object to add * @return true if the bean was added, false if it was already present @@ -309,12 +310,12 @@ public boolean addBean(Object o) { if (o instanceof LifeCycle l) return addBean(o, l.isRunning() ? Managed.UNMANAGED : Managed.AUTO); - return addBean(o, Managed.POJO); } /** - * Adds the given bean, explicitly managing it or not. + *

Adds the given bean, explicitly managing it or not.

+ *

This method should not be called from a constructor, instead use {@link #installBean(Object)}

* * @param o The bean object to add * @param managed whether to manage the lifecycle of the bean @@ -413,6 +414,56 @@ else if (isStarted()) return true; } + /** + * Add a bean in a way that is safe to call from a super constructor of this {@code ContainerLifeCycle}: + * there are no {@link Container.Listener}s registered; + * the object itself is not a {@link Container.Listener}; + * this {@link LifeCycle} is not started or starting; + * and the is no debugging call to {@code this.toString()}. + * @param o The bean to add + * @return true if the bean was added + */ + protected final boolean installBean(Object o) + { + return installBean(o, o instanceof LifeCycle l ? (l.isRunning() ? Managed.UNMANAGED : Managed.AUTO) : Managed.POJO); + } + + /** + * Add a bean in a way that is safe to call from a super constructor of this {@code ContainerLifeCycle}: + * there are no {@link Container.Listener}s registered; + * the object itself is not a {@link Container.Listener}; + * this {@link LifeCycle} is not started or starting; + * and the is no debugging call to {@code this.toString()}. + * @param o The bean to add + * @param managed true if the bean is to be managed. + * @return true if the bean was added + */ + protected final boolean installBean(Object o, boolean managed) + { + return installBean(o, o instanceof LifeCycle + ? (managed ? Managed.MANAGED : Managed.UNMANAGED) + : (managed ? Managed.POJO : Managed.UNMANAGED)); + } + + private boolean installBean(Object o, Managed managed) + { + if (o == null || contains(o)) + return false; + if (o instanceof Container.Listener || !_listeners.isEmpty()) + throw new IllegalArgumentException("Cannot call Container.Listeners from constructor"); + + if (o instanceof EventListener eventListener) + addEventListener(eventListener); + + Bean newBean = new Bean(o); + newBean._managed = managed; + _beans.add(newBean); + + if (LOG.isDebugEnabled()) + LOG.debug("{}@{} added {}", getClass().getSimpleName(), hashCode(), newBean); + return true; + } + /** * Adds a managed lifecycle. *

This is a convenience method that uses addBean(lifecycle,true) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java index b5a956037d47..afa0f7da6ae2 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/AttributeNormalizer.java @@ -29,7 +29,6 @@ import java.util.stream.Stream; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.URIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -411,17 +410,11 @@ private String expand(String prefix, String property, String suffix) { case "WAR", "WAR.path" -> { - Resource r = baseResource.resolve(suffix); - if (r == null) - return prefix + URIUtil.addPaths(baseResource.iterator().next().getPath().toString(), suffix); - return prefix + r.getPath(); + return prefix + baseResource.resolve(suffix).getPath(); } case "WAR.uri" -> { - Resource r = baseResource.resolve(suffix); - if (r == null) - return prefix + URIUtil.addPaths(baseResource.iterator().next().getURI().toString(), suffix); - return prefix + r.getURI(); + return prefix + baseResource.resolve(suffix).getURI(); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java index 516968a9699b..7d22ed2db04b 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/CombinedResource.java @@ -153,20 +153,23 @@ public Resource resolve(String subUriPath) // Attempt a simple (single) Resource lookup that exists Resource resolved = null; + Resource notFound = null; for (Resource res : _resources) { resolved = res.resolve(subUriPath); - if (Resources.missing(resolved)) - continue; // skip, doesn't exist - if (!resolved.isDirectory()) + if (!Resources.missing(resolved) && !resolved.isDirectory()) return resolved; // Return simple (non-directory) Resource + + if (Resources.missing(resolved) && notFound == null) + notFound = resolved; + if (resources == null) resources = new ArrayList<>(); resources.add(resolved); } if (resources == null) - return resolved; // This will not exist + return notFound; // This will not exist if (resources.size() == 1) return resources.get(0); @@ -177,13 +180,28 @@ public Resource resolve(String subUriPath) @Override public boolean exists() { - return _resources.stream().anyMatch(Resource::exists); + for (Resource r : _resources) + if (r.exists()) + return true; + return false; } @Override public Path getPath() { - return null; + int exists = 0; + Path path = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + path = r.getPath(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getPath(); + case 1 -> path; + default -> null; + }; } @Override @@ -213,7 +231,19 @@ else if (!filename.equals(fn)) @Override public URI getURI() { - return null; + int exists = 0; + URI uri = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + uri = r.getURI(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getURI(); + case 1 -> uri; + default -> null; + }; } @Override @@ -289,6 +319,8 @@ public void copyTo(Path destination) throws IOException Collection all = getAllResources(); for (Resource r : all) { + if (!r.exists()) + continue; Path relative = getPathTo(r); Path pathTo = Objects.equals(relative.getFileSystem(), destination.getFileSystem()) ? destination.resolve(relative) @@ -335,6 +367,37 @@ public int hashCode() return Objects.hash(_resources); } + @Override + public boolean isAlias() + { + for (Resource r : _resources) + { + if (r.isAlias()) + return true; + } + return false; + } + + @Override + public URI getRealURI() + { + if (!isAlias()) + return getURI(); + int exists = 0; + URI uri = null; + for (Resource r : _resources) + { + if (r.exists() && exists++ == 0) + uri = r.getRealURI(); + } + return switch (exists) + { + case 0 -> _resources.get(0).getRealURI(); + case 1 -> uri; + default -> null; + }; + } + /** * @return the list of resources */ @@ -375,6 +438,8 @@ public Path getPathTo(Resource other) // return true it's relative location to the first matching resource. for (Resource r : _resources) { + if (!r.exists()) + continue; Path path = r.getPath(); if (otherPath.startsWith(path)) return path.relativize(otherPath); @@ -387,8 +452,14 @@ public Path getPathTo(Resource other) Path relative = null; loop : for (Resource o : other) { + if (!o.exists()) + continue; + for (Resource r : _resources) { + if (!r.exists()) + continue; + if (o.getPath().startsWith(r.getPath())) { Path rel = r.getPath().relativize(o.getPath()); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 14013f9d557f..b095f922e498 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -292,10 +292,7 @@ public Resource resolve(String subUriPath) URI uri = getURI(); URI resolvedUri = URIUtil.addPath(uri, subUriPath); Path path = Paths.get(resolvedUri); - if (Files.exists(path)) - return newResource(path, resolvedUri); - - return null; + return newResource(path, resolvedUri); } /** diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java index 2c739ca225e2..c86d2b10a6be 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResourceFactory.java @@ -14,8 +14,6 @@ package org.eclipse.jetty.util.resource; import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; public class PathResourceFactory implements ResourceFactory @@ -23,9 +21,6 @@ public class PathResourceFactory implements ResourceFactory @Override public Resource newResource(URI uri) { - Path path = Paths.get(uri.normalize()); - if (!Files.exists(path)) - return null; - return new PathResource(path, uri, false); + return new PathResource(Paths.get(uri.normalize()), uri, false); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index 37c417a001ef..ec0dce3ed146 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -268,7 +268,8 @@ public List list() * Resolve an existing Resource. * * @param subUriPath the encoded subUriPath - * @return an existing Resource representing the requested subUriPath, or null if resource does not exist. + * @return a Resource representing the requested subUriPath, which may not {@link #exists() exist}, + * or null if the resource cannot exist. * @throws IllegalArgumentException if subUriPath is invalid */ public abstract Resource resolve(String subUriPath); @@ -303,11 +304,17 @@ public URI getRealURI() public void copyTo(Path destination) throws IOException { + if (!exists()) + throw new IOException("Resource does not exist: " + getFileName()); + Path src = getPath(); if (src == null) { if (!isDirectory()) { + if (Files.isDirectory(destination)) + destination = destination.resolve(getFileName()); + // use old school stream based copy try (InputStream in = newInputStream(); OutputStream out = Files.newOutputStream(destination)) { @@ -327,7 +334,6 @@ public void copyTo(Path destination) // to a directory, preserve the filename Path destPath = destination.resolve(src.getFileName().toString()); Files.copy(src, destPath, - StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } @@ -335,7 +341,6 @@ public void copyTo(Path destination) { // to a file, use destination as-is Files.copy(src, destination, - StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java index 1be738b27889..6f823479d873 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java @@ -68,7 +68,7 @@ public KeyStoreScanner(SslContextFactory sslContextFactory) _scanner.setReportExistingFilesOnStartup(false); _scanner.setScanDepth(1); _scanner.addListener(this); - addBean(_scanner); + installBean(_scanner); } private Path getRealKeyStorePath() diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java index fdc39f85b1b2..275608eb6339 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/MonitoredQueuedThreadPool.java @@ -47,10 +47,10 @@ public MonitoredQueuedThreadPool(int maxThreads) public MonitoredQueuedThreadPool(int maxThreads, int minThreads, int idleTimeOut, BlockingQueue queue) { super(maxThreads, minThreads, idleTimeOut, queue); - addBean(queueStats); - addBean(queueLatencyStats); - addBean(taskLatencyStats); - addBean(threadStats); + installBean(queueStats); + installBean(queueLatencyStats); + installBean(taskLatencyStats); + installBean(threadStats); } @Override diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index c4f5d191c818..2078d92609ea 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -213,9 +213,17 @@ protected void doStart() throws Exception } else { - ReservedThreadExecutor reserved = new ReservedThreadExecutor(this, _reservedThreads); - reserved.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS); - _tryExecutor = reserved; + int reserved = ReservedThreadExecutor.reservedThreads(this, _reservedThreads); + if (reserved == 0) + { + _tryExecutor = NO_TRY; + } + else + { + ReservedThreadExecutor rte = new ReservedThreadExecutor(this, _reservedThreads); + rte.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS); + _tryExecutor = rte; + } } addBean(_tryExecutor); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java index 3f23becf49ac..8a0cbeddb8a7 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java @@ -14,32 +14,22 @@ package org.eclipse.jetty.util.thread; import java.io.IOException; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; -import org.eclipse.jetty.util.AtomicBiInteger; -import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.ProcessorUtils; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; -import org.eclipse.jetty.util.component.DumpableCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static org.eclipse.jetty.util.AtomicBiInteger.getHi; -import static org.eclipse.jetty.util.AtomicBiInteger.getLo; - /** *

A TryExecutor using pre-allocated/reserved threads from an external Executor.

*

Calls to {@link #tryExecute(Runnable)} on ReservedThreadExecutor will either @@ -51,48 +41,68 @@ * the external Executor.

*/ @ManagedObject("A pool for reserved threads") -public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExecutor, Dumpable +public class ReservedThreadExecutor extends ContainerLifeCycle implements TryExecutor, Dumpable { private static final Logger LOG = LoggerFactory.getLogger(ReservedThreadExecutor.class); - private static final long DEFAULT_IDLE_TIMEOUT = TimeUnit.MINUTES.toNanos(1); - private static final Runnable STOP = new Runnable() - { - @Override - public void run() - { - } - - @Override - public String toString() - { - return "STOP"; - } - }; private final Executor _executor; - private final int _capacity; - private final Set _threads = ConcurrentHashMap.newKeySet(); - private final SynchronousQueue _queue = new SynchronousQueue<>(false); - private final AtomicBiInteger _count = new AtomicBiInteger(); // hi=pending; lo=size; - private final AtomicLong _lastEmptyNanoTime = new AtomicLong(NanoTime.now()); + private final ThreadIdPool _threads; + private final AtomicInteger _pending = new AtomicInteger(); + private final int _minSize; + private final int _maxPending; private ThreadPoolBudget.Lease _lease; - private long _idleTimeNanos = DEFAULT_IDLE_TIMEOUT; + private long _idleTimeoutMs; /** * @param executor The executor to use to obtain threads - * @param capacity The number of threads to preallocate. If less than 0 then capacity - * is calculated based on a heuristic from the number of available processors and - * thread pool size. + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. */ public ReservedThreadExecutor(Executor executor, int capacity) + { + this(executor, capacity, -1); + } + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. + * @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value. + */ + public ReservedThreadExecutor(Executor executor, int capacity, int minSize) + { + this(executor, capacity, minSize, -1); + } + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. + * @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value. + * @param maxPending The maximum number of reserved Threads to start, or -1 for no limit. + */ + public ReservedThreadExecutor(Executor executor, int capacity, int minSize, int maxPending) { _executor = executor; - _capacity = reservedThreads(executor, capacity); + _threads = new ThreadIdPool<>(reservedThreads(executor, capacity)); + _minSize = minSize < 0 ? Math.min(1, _threads.capacity()) : minSize; + if (_minSize > _threads.capacity()) + throw new IllegalArgumentException("minSize larger than capacity"); + _maxPending = maxPending; + if (_maxPending == 0) + throw new IllegalArgumentException("maxPending cannot be 0"); if (LOG.isDebugEnabled()) LOG.debug("{}", this); + installBean(_executor); + installBean(_threads); } /** + * Get the heuristic number of reserved threads. + * * @param executor The executor to use to obtain threads * @param capacity The number of threads to preallocate, If less than 0 then capacity * is calculated based on a heuristic from the number of available processors and @@ -100,7 +110,7 @@ public ReservedThreadExecutor(Executor executor, int capacity) * @return the number of reserved threads that would be used by a ReservedThreadExecutor * constructed with these arguments. */ - private static int reservedThreads(Executor executor, int capacity) + public static int reservedThreads(Executor executor, int capacity) { if (capacity >= 0) return capacity; @@ -110,7 +120,7 @@ private static int reservedThreads(Executor executor, int capacity) if (executor instanceof ThreadPool.SizedThreadPool) { int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads(); - return Math.max(1, Math.min(cpus, threads / 10)); + return Math.max(1, TypeUtil.ceilToNextPowerOfTwo(Math.min(cpus, threads / 8))); } return cpus; } @@ -126,7 +136,7 @@ public Executor getExecutor() @ManagedAttribute(value = "max number of reserved threads", readonly = true) public int getCapacity() { - return _capacity; + return _threads.capacity(); } /** @@ -135,19 +145,20 @@ public int getCapacity() @ManagedAttribute(value = "available reserved threads", readonly = true) public int getAvailable() { - return _count.getLo(); + return _threads.size(); } - @ManagedAttribute(value = "pending reserved threads", readonly = true) + @ManagedAttribute(value = "pending reserved threads (deprecated)", readonly = true) + @Deprecated public int getPending() { - return _count.getHi(); + return 0; } @ManagedAttribute(value = "idle timeout in ms", readonly = true) public long getIdleTimeoutMs() { - return NANOSECONDS.toMillis(_idleTimeNanos); + return _idleTimeoutMs; } /** @@ -160,14 +171,13 @@ public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit) { if (isRunning()) throw new IllegalStateException(); - _idleTimeNanos = (idleTime <= 0 || idleTimeUnit == null) ? DEFAULT_IDLE_TIMEOUT : idleTimeUnit.toNanos(idleTime); + _idleTimeoutMs = idleTime <= 0 ? 0 : idleTimeUnit.toMillis(idleTime); } @Override public void doStart() throws Exception { - _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _capacity); - _count.set(0, 0); + _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, getCapacity()); super.doStart(); } @@ -179,27 +189,7 @@ public void doStop() throws Exception super.doStop(); - // Mark this instance as stopped. - int size = _count.getAndSetLo(-1); - - // Offer the STOP task to all waiting reserved threads. - for (int i = 0; i < size; ++i) - { - // Yield to wait for any reserved threads that - // have incremented the size but not yet polled. - Thread.yield(); - _queue.offer(STOP); - } - - // Interrupt any reserved thread missed the offer, - // so they do not wait for the whole idle timeout. - _threads.stream() - .filter(ReservedThread::isReserved) - .map(t -> t._thread) - .filter(Objects::nonNull) - .forEach(Thread::interrupt); - _threads.clear(); - _count.getAndSetHi(0); + _threads.removeAll().forEach(ReservedThread::stop); } @Override @@ -217,225 +207,181 @@ public void execute(Runnable task) throws RejectedExecutionException @Override public boolean tryExecute(Runnable task) { - if (LOG.isDebugEnabled()) - LOG.debug("{} tryExecute {}", this, task); if (task == null) return false; - // Offer will only succeed if there is a reserved thread waiting - boolean offered = _queue.offer(task); - - // If the offer succeeded we need to reduce the size, unless it is set to -1 in the meantime - int size = _count.getLo(); - while (offered && size > 0 && !_count.compareAndSetLo(size, --size)) - size = _count.getLo(); + ReservedThread reserved = _threads.take(); + if (reserved != null) + { + reserved.wakeup(task); + return true; + } - // If size is 0 and we are not stopping, start a new reserved thread - if (size == 0 && task != STOP) - startReservedThread(); + startReservedThread(); - return offered; + if (LOG.isDebugEnabled()) + LOG.debug("{} tryExecute failed for {}", this, task); + return false; } private void startReservedThread() { - while (true) + if (_maxPending > 0 && _pending.incrementAndGet() >= _maxPending) { - long count = _count.get(); - int pending = getHi(count); - int size = getLo(count); - if (size < 0 || pending + size >= _capacity) - return; - if (size == 0) - _lastEmptyNanoTime.set(NanoTime.now()); - if (!_count.compareAndSet(count, pending + 1, size)) - continue; + _pending.decrementAndGet(); + return; + } + try + { + ReservedThread thread = new ReservedThread(); + _executor.execute(thread); + } + catch (Throwable e) + { if (LOG.isDebugEnabled()) - LOG.debug("{} startReservedThread p={}", this, pending + 1); - try - { - ReservedThread thread = new ReservedThread(); - _threads.add(thread); - _executor.execute(thread); - } - catch (Throwable e) - { - _count.add(-1, 0); - if (LOG.isDebugEnabled()) - LOG.debug("ignored", e); - } - return; + LOG.debug("ignored", e); } } @Override public void dump(Appendable out, String indent) throws IOException { - Dumpable.dumpObjects(out, indent, this, - new DumpableCollection("threads", - _threads.stream() - .filter(ReservedThread::isReserved) - .collect(Collectors.toList()))); + Dumpable.dumpObjects(out, indent, this); } @Override public String toString() { - return String.format("%s@%x{reserved=%d/%d,pending=%d}", + return String.format("%s@%x{capacity=%d,threads=%s}", getClass().getSimpleName(), hashCode(), - _count.getLo(), - _capacity, - _count.getHi()); - } - - private enum State - { - PENDING, - RESERVED, - RUNNING, - IDLE, - STOPPED + getCapacity(), + _threads); } private class ReservedThread implements Runnable { - // The state and thread are kept only for dumping - private volatile State _state = State.PENDING; + private final Semaphore _semaphore = new Semaphore(0); + private volatile Runnable _task; private volatile Thread _thread; - private boolean isReserved() - { - return _state == State.RESERVED; - } - - private Runnable reservedWait() - { - if (LOG.isDebugEnabled()) - LOG.debug("{} waiting {}", this, ReservedThreadExecutor.this); - - // Keep waiting until stopped, tasked or idle - while (_count.getLo() >= 0) - { - try - { - // Always poll at some period as safety to ensure we don't poll forever. - Runnable task = _queue.poll(_idleTimeNanos, NANOSECONDS); - if (LOG.isDebugEnabled()) - LOG.debug("{} task={} {}", this, task, ReservedThreadExecutor.this); - if (task != null) - return task; - - // we have idled out - int size = _count.getLo(); - // decrement size if we have not also been stopped. - while (size > 0) - { - if (_count.compareAndSetLo(size, --size)) - break; - size = _count.getLo(); - } - _state = size >= 0 ? State.IDLE : State.STOPPED; - return STOP; - } - catch (InterruptedException e) - { - if (LOG.isDebugEnabled()) - LOG.debug("ignored", e); - } - } - _state = State.STOPPED; - return STOP; - } - @Override public void run() { _thread = Thread.currentThread(); try { + _pending.decrementAndGet(); + while (true) { - long count = _count.get(); + int slot = _threads.offer(this); + if (LOG.isDebugEnabled()) + LOG.debug("offered to slot " + slot); - // reduce pending if this thread was pending - int pending = getHi(count) - (_state == State.PENDING ? 1 : 0); - int size = getLo(count); + if (slot < 0) + // no slot available + return; - State next; - if (size < 0 || size >= _capacity) - { - // The executor has stopped or this thread is excess to capacity - next = State.STOPPED; - } - else + if (!isRunning() && _threads.remove(this, slot)) + return; + + Runnable task = waitForTask(); + while (task == null) { - long now = NanoTime.now(); - long lastEmpty = _lastEmptyNanoTime.get(); - if (size > 0 && _idleTimeNanos < NanoTime.elapsed(lastEmpty, now) && _lastEmptyNanoTime.compareAndSet(lastEmpty, now)) - { - // it has been too long since we hit zero reserved threads, so are "busy" idle - next = State.IDLE; - } - else + if (!isRunning()) + return; + + // Shrink if we are already removed or there are other reserved threads; + // there is a small chance multiple threads will shrink below minSize. + if (getAvailable() > _minSize && _threads.remove(this, slot)) { - // We will become a reserved thread if we can update the count below. - next = State.RESERVED; - size++; + if (LOG.isDebugEnabled()) + LOG.debug("{} reservedThread shrank {}", ReservedThreadExecutor.this, this); + return; } + task = waitForTask(); } - // Update count for pending and size - if (!_count.compareAndSet(count, pending, size)) - continue; - - if (LOG.isDebugEnabled()) - LOG.debug("{} was={} next={} size={}+{} capacity={}", this, _state, next, pending, size, _capacity); - _state = next; - if (next != State.RESERVED) - break; - - // We are reserved whilst we are waiting for an offered _task. - Runnable task = reservedWait(); - - // Is the task the STOP poison pill? - if (task == STOP) - break; - - // Run the task try { - _state = State.RUNNING; + if (LOG.isDebugEnabled()) + LOG.debug("{} reservedThread run {} on {}", ReservedThreadExecutor.this, task, this); task.run(); } - catch (Throwable e) + catch (Throwable t) { - LOG.warn("Unable to run task", e); + if (LOG.isDebugEnabled()) + LOG.debug("{} task {} failure", ReservedThreadExecutor.this, task, t); } finally { - // Clear any interrupted status. - Thread.interrupted(); + // clear interrupted status between reserved thread iterations + if (Thread.interrupted() && LOG.isDebugEnabled()) + LOG.debug("{} was interrupted", _thread); } } } - finally + catch (Throwable t) { if (LOG.isDebugEnabled()) - LOG.debug("{} exited {}", this, ReservedThreadExecutor.this); - _threads.remove(this); + LOG.debug("{} reservedThread {} failure", ReservedThreadExecutor.this, this, t); + } + finally + { _thread = null; } } + private void wakeup(Runnable task) + { + _task = task; + _semaphore.release(); + } + + private Runnable waitForTask() + { + try + { + if (LOG.isDebugEnabled()) + LOG.debug("waiting for task"); + if (_idleTimeoutMs <= 0) + _semaphore.acquire(); + else if (!_semaphore.tryAcquire(_idleTimeoutMs, TimeUnit.MILLISECONDS)) + return null; + Runnable task = _task; + _task = null; + return task; + } + catch (Throwable e) + { + if (LOG.isDebugEnabled()) + LOG.debug("wait failed ", e); + } + return null; + } + + private void stop() + { + // If we are stopping, the reserved thread may already have stopped. So just interrupt rather than + // expect an exchange rendezvous. + _semaphore.release(); + Thread thread = _thread; + if (thread != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("interrupting thread {} for stop", thread); + thread.interrupt(); + } + } + @Override public String toString() { - return String.format("%s@%x{%s,thread=%s}", + return String.format("%s@%x{thread=%s}", getClass().getSimpleName(), hashCode(), - _state, _thread); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadIdPool.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadIdPool.java new file mode 100644 index 000000000000..84f092567bfc --- /dev/null +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadIdPool.java @@ -0,0 +1,251 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.jetty.util.MemoryUtils; +import org.eclipse.jetty.util.ProcessorUtils; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.component.DumpableCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A fixed sized pool of items that uses ThreadId to avoid contention. + * This class can be used, instead of a {@link ThreadLocal}, when pooling items + * that are expensive to create, but only used briefly in the scope of a single thread. + * It is safe to use with {@link org.eclipse.jetty.util.VirtualThreads}, as unlike a {@link ThreadLocal} pool, + * the number of items is limited. + *

This is a light-weight version of {@link org.eclipse.jetty.util.ConcurrentPool} that is best used + * when items do not reserve an index in the pool even when acquired. + * @see org.eclipse.jetty.util.ConcurrentPool + */ +public class ThreadIdPool implements Dumpable +{ + private static final Logger LOG = LoggerFactory.getLogger(ThreadIdPool.class); + + // How far the entries in the AtomicReferenceArray are spread apart to avoid false sharing. + private static final int SPREAD_FACTOR = MemoryUtils.getReferencesPerCacheLine(); + + private final int _capacity; + private final AtomicReferenceArray _items; + + public ThreadIdPool() + { + this(-1); + } + + public ThreadIdPool(int capacity) + { + _capacity = calcCapacity(capacity); + _items = new AtomicReferenceArray<>((_capacity + 1) * SPREAD_FACTOR); + if (LOG.isDebugEnabled()) + LOG.debug("{}", this); + } + + private static int calcCapacity(int capacity) + { + if (capacity >= 0) + return capacity; + return 2 * TypeUtil.ceilToNextPowerOfTwo(ProcessorUtils.availableProcessors()); + } + + private static int toSlot(int index) + { + return (index + 1) * SPREAD_FACTOR; + } + + /** + * @return the maximum number of items + */ + public int capacity() + { + return _capacity; + } + + /** + * @return the number of items available + */ + public int size() + { + int available = 0; + for (int i = 0; i < capacity(); i++) + { + if (_items.getPlain(toSlot(i)) != null) + available++; + } + return available; + } + + /** + * Offer an item to the pool. + * @param e The item to offer + * @return The index the item was added at or -1, if it was not added + * @see #remove(Object, int) + */ + public int offer(E e) + { + int capacity = capacity(); + if (capacity > 0) + { + int index = (int)(Thread.currentThread().getId() % capacity); + for (int i = 0; i < capacity; i++) + { + if (_items.compareAndSet(toSlot(index), null, e)) + return index; + if (++index == capacity) + index = 0; + } + } + return -1; + } + + /** + * Take an item from the pool. + * @return The taken item or null if none available. + */ + public E take() + { + int capacity = capacity(); + if (capacity == 0) + return null; + int index = (int)(Thread.currentThread().getId() % capacity); + for (int i = 0; i < capacity; i++) + { + E e = _items.getAndSet(toSlot(index), null); + if (e != null) + return e; + if (++index == capacity) + index = 0; + } + return null; + } + + /** + * Remove a specific item from the pool from a specific index + * @param e The item to remove + * @param index The index the item was given to, as returned by {@link #offer(Object)} + * @return {@code True} if the item was in the pool and was able to be removed. + */ + public boolean remove(E e, int index) + { + if (index < 0) + throw new IndexOutOfBoundsException(); + return _items.compareAndSet(toSlot(index), e, null); + } + + /** + * Removes all items from the pool. + * @return A list of all removed items + */ + public List removeAll() + { + int capacity = capacity(); + List all = new ArrayList<>(capacity); + for (int i = 0; i < capacity; i++) + { + E e = _items.getAndSet(toSlot(i), null); + if (e != null) + all.add(e); + } + return all; + } + + /** + * Take an item with a {@link #take()} operation, else if that returns null then use the {@code supplier} (which may + * construct a new instance). + * @param supplier The supplier for an item to be used if an item cannot be taken from the pool. + * @return An item, never null. + */ + public E takeOrElse(Supplier supplier) + { + E e = take(); + return e == null ? supplier.get() : e; + } + + /** + * Apply an item, either from the pool or supplier, to a function, then give it back to the pool. + * This is equivalent of {@link #takeOrElse(Supplier)}; then {@link Function#apply(Object)}; + * followed by {@link #offer(Object)}. + * @param supplier The supplier for an item to be used if an item cannot be taken from the pool. + * @param function A function producing a result from an item. This may be + * a method reference to a method on the item taking no arguments and producing a result. + * @param The type of the function return + * @return Te result of the function applied to the item and the argument + */ + public R apply(Supplier supplier, Function function) + { + E e = takeOrElse(supplier); + try + { + return function.apply(e); + } + finally + { + offer(e); + } + } + + /** + * Apply an item, either from the pool or supplier, to a function, then give it back to the pool. + * This is equivalent of {@link #takeOrElse(Supplier)}; then {@link BiFunction#apply(Object, Object)}; + * followed by {@link #offer(Object)}. + * @param supplier The supplier for an item to be used if an item cannot be taken from the pool. + * @param function A function producing a result from an item and an argument. This may be + * a method reference to a method on the item taking an argument and producing a result. + * @param argument The argument to pass to the function. + * @param The type of the function argument + * @param The type of the function return + * @return Te result of the function applied to the item and the argument + */ + public R apply(Supplier supplier, BiFunction function, A argument) + { + E e = takeOrElse(supplier); + try + { + return function.apply(e, argument); + } + finally + { + offer(e); + } + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + int capacity = capacity(); + List slots = new ArrayList<>(capacity); + for (int i = 0; i < capacity; i++) + slots.add(_items.get(toSlot(i))); + Dumpable.dumpObjects(out, indent, this, new DumpableCollection("items", slots)); + } + + @Override + public String toString() + { + return String.format("%s@%x{capacity=%d}", + getClass().getSimpleName(), + hashCode(), + capacity()); + } +} diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/AdaptiveExecutionStrategy.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/AdaptiveExecutionStrategy.java index 5328694a63c2..5e0850e649fb 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/AdaptiveExecutionStrategy.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/thread/strategy/AdaptiveExecutionStrategy.java @@ -145,9 +145,9 @@ public AdaptiveExecutionStrategy(Producer producer, Executor executor) _executor = executor; _tryExecutor = TryExecutor.asTryExecutor(executor); _virtualExecutor = VirtualThreads.getVirtualThreadsExecutor(_executor); - addBean(_producer); - addBean(_tryExecutor); - addBean(_virtualExecutor); + installBean(_producer); + installBean(_tryExecutor); + installBean(_virtualExecutor); if (LOG.isDebugEnabled()) LOG.debug("{} created", this); } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java index 3e70c7f892d7..8432153365c3 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java @@ -31,6 +31,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -254,4 +255,16 @@ public void testToShortName(Class clazz, String shortName) assertThat(TypeUtil.toShortName(clazz), is(shortName)); } + @Test + public void testCeilNextPowerOfTwo() + { + assertThrows(IllegalArgumentException.class, () -> TypeUtil.ceilToNextPowerOfTwo(-1)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(0), is(1)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(1), is(1)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(2), is(2)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(3), is(4)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(4), is(4)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(5), is(8)); + assertThat(TypeUtil.ceilToNextPowerOfTwo(Integer.MAX_VALUE - 1), is(Integer.MAX_VALUE)); + } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java index 24be85559d86..dd3854360fd5 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/LifeCycleListenerTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class LifeCycleListenerTest @@ -222,4 +223,58 @@ public void lifeCycleStopping(LifeCycle event) } } } + + @Test + public void testInstallBeanNotAListener() + { + Container.Listener listener = new TestContainerListener(); + + assertThrows(IllegalArgumentException.class, () -> new ContainerLifeCycle() + { + { + installBean(listener); + } + }); + } + + @Test + public void testInstallBeanNoListeners() + { + Container.Listener listener = new TestContainerListener(); + + assertThrows(IllegalArgumentException.class, () -> new ContainerLifeCycle() + { + { + addEventListener(listener); + installBean("test"); + } + }); + } + + @Test + public void testInstallBean() + { + assertEquals("test", + new ContainerLifeCycle() + { + { + installBean("test"); + } + }.getBean(String.class)); + } + + private static class TestContainerListener implements Container.Listener + { + @Override + public void beanAdded(Container parent, Object child) + { + + } + + @Override + public void beanRemoved(Container parent, Object child) + { + + } + } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java index f45c7a0ee9e6..17b5f2a468ee 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java @@ -49,7 +49,6 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; @ExtendWith(WorkDirExtension.class) public class CombinedResourceTest @@ -118,7 +117,7 @@ public void testList() throws Exception assertThat(relative, containsInAnyOrder(expected)); Resource unk = rc.resolve("unknown"); - assertNull(unk); + assertFalse(unk.exists()); assertEquals(getContent(rc, "1.txt"), "1 - one"); assertEquals(getContent(rc, "2.txt"), "2 - two"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MemoryResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MemoryResourceTest.java index 3ff65f4556d8..6889e8a18f8e 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MemoryResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/MemoryResourceTest.java @@ -13,22 +13,57 @@ package org.eclipse.jetty.util.resource; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Loader; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(WorkDirExtension.class) public class MemoryResourceTest { + public WorkDir workDir; + @Test public void testJettyLogging() throws Exception { Resource resource = ResourceFactory.root().newMemoryResource(Loader.getResource("jetty-logging.properties")); assertTrue(resource.exists()); - String contents = IO.toString(resource.newInputStream()); - assertThat(contents, startsWith("#org.eclipse.jetty.util.LEVEL=DEBUG")); + try (InputStream in = resource.newInputStream()) + { + String contents = IO.toString(in); + assertThat(contents, startsWith("#org.eclipse.jetty.util.LEVEL=DEBUG")); + } + } + + @Test + public void testCopyToFile() throws Exception + { + Resource resource = ResourceFactory.root().newMemoryResource(Loader.getResource("jetty-logging.properties")); + Path targetFile = workDir.getEmptyPathDir().resolve("target-jetty-logging.properties"); + resource.copyTo(targetFile); + + assertThat(Files.exists(targetFile), is(true)); + } + + @Test + public void testCopyToFolder() throws Exception + { + Resource resource = ResourceFactory.root().newMemoryResource(Loader.getResource("jetty-logging.properties")); + Path targetDir = workDir.getEmptyPathDir(); + resource.copyTo(targetDir); + + Path resolved = targetDir.resolve("jetty-logging.properties"); + assertThat(Files.exists(resolved), is(true)); } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java index a183776aec6a..c0739721063c 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java @@ -406,7 +406,7 @@ public void testJarFileIsAliasFile(WorkDir workDir) throws IOException // Resolve to name, but different case testText = archiveResource.resolve("/TEST.TXT"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve using path navigation testText = archiveResource.resolve("/foo/../test.txt"); @@ -464,9 +464,9 @@ public void testJarFileIsAliasDirectory(WorkDir workDir) throws IOException // Resolve file to name, but different case testText = archiveResource.resolve("/dir/TEST.TXT"); - assertNull(testText); + assertFalse(testText.exists()); testText = archiveResource.resolve("/DIR/test.txt"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve file using path navigation testText = archiveResource.resolve("/foo/../dir/test.txt"); @@ -480,7 +480,7 @@ public void testJarFileIsAliasDirectory(WorkDir workDir) throws IOException // Resolve file using extension-less directory testText = archiveResource.resolve("/dir./test.txt"); - assertNull(testText); + assertFalse(testText.exists()); // Resolve directory to name, no slash Resource dirResource = archiveResource.resolve("/dir"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index e690b8ee9dff..a3d939a2a518 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -21,7 +21,9 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; import java.util.stream.Stream; @@ -61,6 +63,8 @@ public class ResourceTest private static final boolean EXISTS = true; private static final ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable(); + public WorkDir workDir; + @AfterAll public static void afterAll() { @@ -263,7 +267,7 @@ public void testResourceExists(Scenario data) if (data.exists) assertThat("Exists: " + res.getName(), res.exists(), equalTo(data.exists)); else - assertNull(res); + assertFalse(res.exists()); } @ParameterizedTest @@ -287,6 +291,75 @@ public void testResourceContent(Scenario data) assertThat("Content: " + data.test, c, startsWith(data.content)); } + @ParameterizedTest + @MethodSource("scenarios") + public void testResourceCopyToDirectory(Scenario data) + throws Exception + { + Resource resource = data.getResource(); + Assumptions.assumeTrue(resource != null); + + Path targetDir = workDir.getEmptyPathDir(); + if (Resources.exists(resource)) + { + resource.copyTo(targetDir); + Path targetToTest = resource.isDirectory() ? targetDir : targetDir.resolve(resource.getFileName()); + assertResourceSameAsPath(resource, targetToTest); + } + else + { + assertThrows(IOException.class, () -> resource.copyTo(targetDir)); + } + } + + @ParameterizedTest + @MethodSource("scenarios") + public void testResourceCopyToFile(Scenario data) + throws Exception + { + Resource resource = data.getResource(); + Assumptions.assumeTrue(resource != null); + Assumptions.assumeFalse(resource.isDirectory()); + + String filename = resource.getFileName(); + Path targetDir = workDir.getEmptyPathDir(); + Path targetFile = targetDir.resolve(filename); + if (Resources.exists(resource)) + { + resource.copyTo(targetFile); + assertResourceSameAsPath(resource, targetFile); + } + else + { + assertThrows(IOException.class, () -> resource.copyTo(targetFile)); + } + } + + @Test + public void testNonExistentResource() + { + Path nonExistentFile = workDir.getPathFile("does-not-exists"); + Resource resource = resourceFactory.newResource(nonExistentFile); + assertFalse(resource.exists()); + assertThrows(IOException.class, () -> resource.copyTo(workDir.getEmptyPathDir())); + assertTrue(resource.list().isEmpty()); + assertFalse(resource.contains(resourceFactory.newResource(workDir.getPath()))); + assertEquals("does-not-exists", resource.getFileName()); + assertFalse(resource.isReadable()); + assertEquals(nonExistentFile, resource.getPath()); + assertEquals(Instant.EPOCH, resource.lastModified()); + assertEquals(0L, resource.length()); + assertThrows(IOException.class, resource::newInputStream); + assertThrows(IOException.class, resource::newReadableByteChannel); + assertEquals(nonExistentFile.toUri(), resource.getURI()); + assertFalse(resource.isAlias()); + assertNull(resource.getRealURI()); + assertNotNull(resource.getName()); + Resource subResource = resource.resolve("does-not-exist-too"); + assertFalse(subResource.exists()); + assertEquals(nonExistentFile.resolve("does-not-exist-too"), subResource.getPath()); + } + @Test public void testGlobPath() { @@ -395,7 +468,7 @@ public void testNewResourcePathDoesNotExist(WorkDir workDir) Path dir = workDir.getEmptyPathDir().resolve("foo/bar"); // at this point we have a directory reference that does not exist Resource resource = resourceFactory.newResource(dir); - assertNull(resource); + assertFalse(resource.exists()); } @Test @@ -407,7 +480,7 @@ public void testNewResourceFileDoesNotExists(WorkDir workDir) throws IOException // at this point we have a file reference that does not exist assertFalse(Files.exists(file)); Resource resource = resourceFactory.newResource(file); - assertNull(resource); + assertFalse(resource.exists()); } @Test @@ -466,4 +539,28 @@ public void testJrtResourceAllModules() assertThat(resource.isDirectory(), is(true)); assertThat(resource.length(), is(0L)); } + + private static void assertResourceSameAsPath(Resource resource, Path copy) throws IOException + { + if (!resource.isDirectory()) + { + assertFalse(Files.isDirectory(copy), "Resource is not dir (" + resource + "), copy is dir (" + copy + ")"); + try (InputStream sourceIs = resource.newInputStream(); + InputStream targetIs = Files.newInputStream(copy)) + { + String source = IO.toString(sourceIs); + String target = IO.toString(targetIs); + assertEquals(source, target, "Resource (" + resource + ") and copy (" + copy + ") contents do not match"); + } + } + else + { + assertTrue(Files.isDirectory(copy), "Resource is dir (" + resource + "), copy is not dir (" + copy + ")"); + List subResources = resource.list(); + for (Resource subResource : subResources) + { + assertResourceSameAsPath(subResource, copy.resolve(subResource.getFileName())); + } + } + } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java index bd19407acb19..f790d5472230 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java @@ -808,11 +808,9 @@ public void testDump() throws Exception waitForIdle(pool, 3); Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle dump = pool.dump(); - assertThat(count(dump, " - STARTED"), is(2)); + assertThat(count(dump, " - STARTED"), is(3)); assertThat(dump, containsString(",3<=3<=4,i=3,r=2,")); assertThat(dump, containsString("[ReservedThreadExecutor@")); - assertThat(count(dump, " IDLE"), is(3)); - assertThat(count(dump, " RESERVED"), is(0)); CountDownLatch started = new CountDownLatch(1); CountDownLatch waiting = new CountDownLatch(1); @@ -828,42 +826,44 @@ public void testDump() throws Exception e.printStackTrace(); } }); + pool.tryExecute(() -> {}); started.await(); Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle dump = pool.dump(); - assertThat(count(dump, " - STARTED"), is(2)); - assertThat(dump, containsString(",3<=3<=4,i=2,r=2,")); + assertThat(count(dump, " - STARTED"), is(3)); + assertThat(dump, containsString(",3<=3<=4,i=1,r=2,")); assertThat(dump, containsString("[ReservedThreadExecutor@")); - assertThat(count(dump, " IDLE"), is(2)); - assertThat(count(dump, " WAITING"), is(1)); - assertThat(count(dump, " RESERVED"), is(0)); + assertThat(count(dump, "> ReservedThread@"), is(1)); + assertThat(count(dump, "> null"), is(1)); assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(0)); pool.setDetailedDump(true); dump = pool.dump(); - assertThat(count(dump, " - STARTED"), is(2)); - assertThat(dump, containsString(",3<=3<=4,i=2,r=2,")); - assertThat(dump, containsString("reserved=0/2")); + assertThat(count(dump, " - STARTED"), is(3)); + assertThat(dump, containsString(",3<=3<=4,i=1,r=2,")); assertThat(dump, containsString("[ReservedThreadExecutor@")); - assertThat(count(dump, " IDLE"), is(2)); - assertThat(count(dump, " WAITING"), is(1)); - assertThat(count(dump, " RESERVED"), is(0)); assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(1)); - assertFalse(pool.tryExecute(() -> + CountDownLatch latch = new CountDownLatch(1); + assertTrue(pool.tryExecute(() -> { + try + { + latch.await(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } })); - waitForReserved(pool, 1); Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle dump = pool.dump(); - assertThat(count(dump, " - STARTED"), is(2)); + assertThat(count(dump, " - STARTED"), is(3)); assertThat(dump, containsString(",3<=3<=4,i=1,r=2,")); - assertThat(dump, containsString("reserved=1/2")); assertThat(dump, containsString("[ReservedThreadExecutor@")); - assertThat(count(dump, " IDLE"), is(1)); - assertThat(count(dump, " WAITING"), is(1)); - assertThat(count(dump, " RESERVED"), is(1)); - assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(1)); + assertThat(count(dump, "> ReservedThread@"), is(0)); + assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(2)); + latch.countDown(); } @Test diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java index a19f12e49518..d6a81b8ce958 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java @@ -15,6 +15,8 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -26,13 +28,12 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.waitAtMost; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class ReservedThreadExecutorTest @@ -78,37 +79,41 @@ public void testStarted() } @Test - public void testPending() throws Exception + public void testInterruptedFlagCleared() { - assertThat(_executor._queue.size(), is(0)); - + // Prime the reserved executor. for (int i = 0; i < SIZE; i++) { - _reservedExecutor.tryExecute(NOOP); + assertFalse(_reservedExecutor.tryExecute(NOOP)); } - assertThat(_executor._queue.size(), is(SIZE)); - for (int i = 0; i < SIZE; i++) { _executor.startThread(); } assertThat(_executor._queue.size(), is(0)); - waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); + // Execute tasks that leave the interrupted flag to true. for (int i = 0; i < SIZE; i++) { - assertThat(_reservedExecutor.tryExecute(new Task()), is(true)); + assertTrue(_reservedExecutor.tryExecute(() -> Thread.currentThread().interrupt())); } - assertThat(_executor._queue.size(), is(1)); - assertThat(_reservedExecutor.getAvailable(), is(0)); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); + // Check that the interrupted flag was cleared. + List interruptedFlags = new CopyOnWriteArrayList<>(); for (int i = 0; i < SIZE; i++) { - assertThat(_reservedExecutor.tryExecute(NOOP), is(false)); + assertTrue(_reservedExecutor.tryExecute(() -> + { + boolean interrupted = Thread.interrupted(); + interruptedFlags.add(interrupted); + })); } - assertThat(_executor._queue.size(), is(SIZE)); - assertThat(_reservedExecutor.getAvailable(), is(0)); + waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); + + assertThat(interruptedFlags.size(), is(SIZE)); + assertThat(interruptedFlags.stream().allMatch(interrupted -> interrupted == false), is(true)); } @Test @@ -118,41 +123,50 @@ public void testExecuted() throws Exception for (int i = 0; i < SIZE; i++) { - _reservedExecutor.tryExecute(NOOP); + // No reserved thread available, so task should be executed and a reserve thread started + assertFalse(_reservedExecutor.tryExecute(NOOP)); } assertThat(_executor._queue.size(), is(SIZE)); for (int i = 0; i < SIZE; i++) { + // start executor threads, which should be 2 reserved thread jobs _executor.startThread(); } assertThat(_executor._queue.size(), is(0)); + // check that the reserved thread pool grows to 2 threads waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); Task[] tasks = new Task[SIZE]; for (int i = 0; i < SIZE; i++) { tasks[i] = new Task(); + // submit a job that will take a reserved thread. assertThat(_reservedExecutor.tryExecute(tasks[i]), is(true)); } for (int i = 0; i < SIZE; i++) { + // wait for the job to run tasks[i]._ran.await(10, SECONDS); } - assertThat(_executor._queue.size(), is(1)); + // This RTP only starts new reserved threads when it there is a miss + assertThat(_executor._queue.size(), is(0)); - Task extra = new Task(); - assertThat(_reservedExecutor.tryExecute(extra), is(false)); - assertThat(_executor._queue.size(), is(2)); + // and we have no reserved threads + assertThat(_reservedExecutor.getAvailable(), is(0)); - waitAtMost(5, SECONDS).until(extra._ran::getCount, is(1L)); + // Complete the jobs for (int i = 0; i < SIZE; i++) + { + // wait for the job to run tasks[i]._complete.countDown(); + } + // reserved threads should run job and then become reserved again waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(SIZE)); } @@ -177,84 +191,7 @@ public void testEvict() throws Exception int available = _reservedExecutor.getAvailable(); assertThat(available, is(2)); - waitAtMost(5 * IDLE, MILLISECONDS).until(_reservedExecutor::getAvailable, is(0)); - } - - @Test - public void testBusyEvict() throws Exception - { - final long IDLE = 1000; - - _reservedExecutor.stop(); - _reservedExecutor.setIdleTimeout(IDLE, MILLISECONDS); - _reservedExecutor.start(); - assertThat(_reservedExecutor.getAvailable(), is(0)); - - assertThat(_reservedExecutor.tryExecute(NOOP), is(false)); - assertThat(_reservedExecutor.tryExecute(NOOP), is(false)); - - _executor.startThread(); - _executor.startThread(); - - waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(2)); - - int available = _reservedExecutor.getAvailable(); - assertThat(available, is(2)); - - CountDownLatch latch = new CountDownLatch(1); - assertThat(_reservedExecutor.tryExecute(() -> - { - try - { - latch.await(); - } - catch (InterruptedException e) - { - throw new RuntimeException(e); - } - }), is(true)); - - // Submit tasks for a period of one idle timeout. - // One reserved thread is busy above waiting on the - // latch, while this reserved thread will be on/off - // busy with short tasks. It will not be evicted - // because it is the only available reserved thread. - for (int i = 0; i < 10; i++) - { - Thread.sleep(IDLE / 10); - assertThat(_reservedExecutor.tryExecute(NOOP), is(true)); - } - waitAtMost(10, SECONDS).until(_reservedExecutor::getAvailable, is(1)); - - latch.countDown(); - - // The reserved thread that run the task that - // awaited must have been immediately evicted, - // because the other threads is available and - // reserved threads are aggressively evicted. - await() - .atLeast(IDLE / 4, MILLISECONDS) - .atMost(10, SECONDS) - .until(_reservedExecutor::getAvailable, is(1)); - } - - @Test - public void testReservedIdleTimeoutWithOneReservedThread() throws Exception - { - long idleTimeout = 500; - _reservedExecutor.stop(); - _reservedExecutor.setIdleTimeout(idleTimeout, MILLISECONDS); - _reservedExecutor.start(); - - assertThat(_reservedExecutor.tryExecute(NOOP), is(false)); - Thread thread = _executor.startThread(); - assertNotNull(thread); - // watching the available thread count decrease relies on the waitAtMost sampling rate being - // faster than the idleTimeout, so it always sees the 1 before the 0 - waitAtMost(5, SECONDS).until(_reservedExecutor::getAvailable, is(1)); - waitAtMost(5, SECONDS).until(_reservedExecutor::getAvailable, is(0)); - thread.join(2 * idleTimeout); - assertFalse(thread.isAlive()); + waitAtMost(5 * IDLE, MILLISECONDS).until(_reservedExecutor::getAvailable, lessThanOrEqualTo(1)); } private static class TestExecutor implements Executor diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java b/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java index 29f212390ffe..967c20f4df91 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java @@ -53,7 +53,7 @@ public WebSocketCoreClient(WebSocketComponents webSocketComponents) public WebSocketCoreClient(HttpClient httpClient, WebSocketComponents webSocketComponents) { client = Objects.requireNonNullElse(httpClient, HttpClientProvider.get()); - addBean(client); + installBean(client); if (webSocketComponents == null) { if (client.isStarted()) @@ -62,7 +62,7 @@ public WebSocketCoreClient(HttpClient httpClient, WebSocketComponents webSocketC webSocketComponents = new WebSocketComponents(); } components = webSocketComponents; - addBean(components); + installBean(components); if (!client.isStarted()) { if (client.getByteBufferPool() == null) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java index 9ff744b0530a..9c6965ecfa9c 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java @@ -69,12 +69,12 @@ public WebSocketComponents(WebSocketExtensionRegistry extensionRegistry, Decorat _executor = executor; } - addBean(_inflaterPool); - addBean(_deflaterPool); - addBean(_bufferPool); - addBean(_extensionRegistry); - addBean(_objectFactory); - addBean(_executor); + installBean(_inflaterPool); + installBean(_deflaterPool); + installBean(_bufferPool); + installBean(_extensionRegistry); + installBean(_objectFactory); + installBean(_executor); } public ByteBufferPool getByteBufferPool() diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketUpgradeHandler.java b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketUpgradeHandler.java index 0c6266b03a42..4eef2672fe48 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketUpgradeHandler.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketUpgradeHandler.java @@ -49,7 +49,7 @@ public WebSocketUpgradeHandler(WebSocketComponents components, Consumer + + commons-codec + commons-codec + test + org.codehaus.plexus plexus-utils diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java index ab9ba579d5fb..6521a2624d4b 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java @@ -81,7 +81,7 @@ public WebSocketClient(HttpClient httpClient) addManaged(coreClient); frameHandlerFactory = new JettyWebSocketFrameHandlerFactory(this, coreClient.getWebSocketComponents()); sessionListeners.add(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public CompletableFuture connect(Object websocket, URI toUri) throws IOException diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/ServerWebSocketContainer.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/ServerWebSocketContainer.java index 1b0ea60bf543..12b23489b02f 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/ServerWebSocketContainer.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/ServerWebSocketContainer.java @@ -131,7 +131,7 @@ public static ServerWebSocketContainer get(Context context) this.mappings = mappings; this.factory = new ServerFrameHandlerFactory(this, mappings.getWebSocketComponents()); addSessionListener(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java index d61b11d5b768..419fc0d9ebf5 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java @@ -45,6 +45,7 @@ import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.thread.AutoLock; +import org.eclipse.jetty.util.thread.ThreadIdPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,7 +124,7 @@ enum ApiState } private static final Logger LOG = LoggerFactory.getLogger(HttpOutput.class); - private static final ThreadLocal _encoder = new ThreadLocal<>(); + private static final ThreadIdPool _encoder = new ThreadIdPool<>(); private final ServletChannel _servletChannel; private final ServletChannelState _channelState; @@ -1005,17 +1006,12 @@ private void print(String s, boolean eoln) throws IOException s = String.valueOf(s); String charset = _servletChannel.getServletContextResponse().getCharacterEncoding(false); - CharsetEncoder encoder = _encoder.get(); + CharsetEncoder encoder = _encoder.take(); if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset)) { encoder = Charset.forName(charset).newEncoder(); encoder.onMalformedInput(CodingErrorAction.REPLACE); encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - _encoder.set(encoder); - } - else - { - encoder.reset(); } ByteBufferPool pool = _servletChannel.getRequest().getComponents().getByteBufferPool(); RetainableByteBuffer out = pool.acquire((int)(1 + (s.length() + 2) * encoder.averageBytesPerChar()), false); @@ -1073,6 +1069,8 @@ else if (crlf != null && crlf.hasRemaining()) finally { out.release(); + encoder.reset(); + _encoder.offer(encoder); } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index f2d54f384104..27bb02f2b1ee 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -284,7 +284,7 @@ public ServletContextHandler(String contextPath, SessionHandler sessionHandler, setErrorHandler(errorHandler); _objFactory = new DecoratedObjectFactory(); - addBean(_objFactory, true); + installBean(_objFactory, true); // Link the handlers relinkHandlers(); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java index 205e918d66ed..45e832edb970 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletTester.java @@ -220,7 +220,7 @@ public ServletTester(String contextPath, int options) _context = new ServletContextHandler(contextPath, options); _server.setHandler(_context); _server.setConnectors(new Connector[]{_connector}); - addBean(_server); + installBean(_server); } public ServletContextHandler getContext() diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java index 381e06d473aa..682787744d76 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/AsyncServletTest.java @@ -40,6 +40,7 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Request; @@ -103,7 +104,7 @@ public void setUp() throws Exception _server.setHandler(new EventsHandler(context) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { _latch.countDown(); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java index 2556e4977917..dbcf5ebc64e0 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletRequestLogTest.java @@ -30,6 +30,7 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Connector; @@ -306,7 +307,7 @@ public void testLogHandlerCollection(Servlet testServlet, String requestPath, St server.setHandler(new EventsHandler(contexts) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { assertRequestLog(expectedLogEntry, captureLog); } @@ -380,7 +381,7 @@ public void testLogHandlerCollectionErrorHandlerServerBean(Servlet testServlet, server.setHandler(new EventsHandler(contexts) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { assertRequestLog(expectedLogEntry, captureLog); } @@ -451,7 +452,7 @@ public void testLogHandlerCollectionSimpleErrorPageMapping(Servlet testServlet, server.setHandler(new EventsHandler(contexts) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { assertRequestLog(expectedLogEntry, captureLog); } @@ -529,7 +530,7 @@ public void testLogHandlerWrapped(Servlet testServlet, String requestPath, Strin server.setHandler(new EventsHandler(contexts) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { assertRequestLog(expectedLogEntry, captureLog); } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-loginservice/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-loginservice/pom.xml index 0c2874cd94d3..b99a319d5a8e 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-loginservice/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-loginservice/pom.xml @@ -29,6 +29,11 @@ org.eclipse.jetty.ee10 jetty-ee10-webapp + + commons-codec + commons-codec + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-infinispan/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-infinispan/pom.xml index 7844bc82de0a..e5950a811306 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-infinispan/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-infinispan/pom.xml @@ -20,6 +20,11 @@ ${gson.version} test + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-jdbc/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-jdbc/pom.xml index b408cd4a7a7f..9b57d0680a62 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-jdbc/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-jdbc/pom.xml @@ -12,6 +12,11 @@ ${project.groupId}.sessions.jdbc + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/ClassMatcher.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/ClassMatcher.java index f7334e8c337f..a79a8783fb9a 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/ClassMatcher.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/ClassMatcher.java @@ -751,6 +751,9 @@ public boolean match(Class clazz) public boolean match(String name, URL url) { + if (url == null) + return false; + // Strip class suffix for name matching if (name.endsWith(".class")) name = name.substring(0, name.length() - 6); diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index 7e89fec12c38..0da93aa322fb 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -1220,14 +1220,14 @@ protected void startContext() { //resolve the metadata _metadata.resolve(this); - super.startContext(); + startWebapp(); } } @Override protected void stopContext() throws Exception { - super.stopContext(); + stopWebapp(); try { for (int i = _configurations.size(); i-- > 0; ) @@ -1253,6 +1253,24 @@ protected void stopContext() throws Exception } } + /** + * Continue the {@link #startContext()} before calling {@code super.startContext()}. + * @throws Exception If there was a problem starting + */ + protected void startWebapp() throws Exception + { + super.startContext(); + } + + /** + * Continue the {@link #stopContext()} before calling {@code super.stopContext()}. + * @throws Exception If there was a problem stopping + */ + protected void stopWebapp() throws Exception + { + super.stopContext(); + } + @Override public Set setServletSecurity(Dynamic registration, ServletSecurityElement servletSecurityElement) { @@ -1364,7 +1382,7 @@ public URL getResource(String path) throws MalformedURLException // If a WAR file is mounted, or is extracted to a temp directory, // then the first entry of the resource base must be the WAR file. Resource resource = WebAppContext.this.getResource(path); - if (resource == null) + if (Resources.missing(resource)) return null; for (Resource r: resource) diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketContainer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketContainer.java index ce2d71a1bd8d..c915d64cf816 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketContainer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee10/websocket/jakarta/common/JakartaWebSocketContainer.java @@ -46,7 +46,7 @@ public JakartaWebSocketContainer(WebSocketComponents components) { this.components = components; addSessionListener(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public abstract Executor getExecutor(); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java index 601ee44a1014..d9e329f0cdbb 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee10/websocket/server/JettyWebSocketServerContainer.java @@ -134,10 +134,10 @@ public String toString() this.components = components; this.executor = executor; this.frameHandlerFactory = new JettyServerFrameHandlerFactory(this, components); - addBean(frameHandlerFactory); + installBean(frameHandlerFactory); addSessionListener(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public void addMapping(String pathSpec, JettyWebSocketCreator creator) diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java index 46e93aa6deaa..755b4a1889cb 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-tests/src/test/java/org/eclipse/jetty/ee10/websocket/tests/WebSocketOverHTTP2Test.java @@ -38,6 +38,7 @@ import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.ee10.websocket.server.internal.DelegatedServerUpgradeRequest; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.HTTP2Cipher; @@ -129,7 +130,7 @@ private void startServer(TestJettyWebSocketServlet servlet) throws Exception server.setHandler(new EventsHandler(server.getHandler()) { @Override - protected void onComplete(Request request, Throwable failure) + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) { if (onComplete != null) onComplete.run(); diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java index c6eee3a973c5..b6627296f501 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java @@ -267,7 +267,7 @@ protected ContextHandler(APIContext context, String contextPath) { _coreContextHandler = new CoreContextHandler(); - addBean(_coreContextHandler, false); + installBean(_coreContextHandler, false); _apiContext = context == null ? new APIContext() : context; _initParams = new HashMap<>(); if (contextPath != null) @@ -2539,7 +2539,7 @@ public class CoreContextHandler extends org.eclipse.jetty.server.handler.Context CoreContextHandler() { super.setHandler(new CoreToNestedHandler()); - addBean(ContextHandler.this, true); + installBean(ContextHandler.this, true); } @Override diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java index 62744880a717..602b417e88f7 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java @@ -44,6 +44,7 @@ import org.eclipse.jetty.util.SharedBlockingCallback; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; import org.eclipse.jetty.util.thread.AutoLock; +import org.eclipse.jetty.util.thread.ThreadIdPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,7 +177,7 @@ default void resetBuffer() throws IllegalStateException } private static final Logger LOG = LoggerFactory.getLogger(HttpOutput.class); - private static final ThreadLocal _encoder = new ThreadLocal<>(); + private static final ThreadIdPool _encoder = new ThreadIdPool<>(); private final HttpChannel _channel; private final HttpChannelState _channelState; @@ -1069,13 +1070,12 @@ private void print(String s, boolean eoln) throws IOException s = String.valueOf(s); String charset = _channel.getResponse().getCharacterEncoding(); - CharsetEncoder encoder = _encoder.get(); + CharsetEncoder encoder = _encoder.take(); if (encoder == null || !encoder.charset().name().equalsIgnoreCase(charset)) { encoder = Charset.forName(charset).newEncoder(); encoder.onMalformedInput(CodingErrorAction.REPLACE); encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - _encoder.set(encoder); } else { @@ -1136,6 +1136,8 @@ else if (crlf != null && crlf.hasRemaining()) finally { out.release(); + encoder.reset(); + _encoder.offer(encoder); } } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java index 1edaa2b7fe1f..2bfb27344213 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java @@ -69,11 +69,11 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab public SessionHandler() { setSessionTrackingModes(DEFAULT_SESSION_TRACKING_MODES); - addBean(_sessionManager); - addBean(_cookieConfig); - addBean(_sessionListeners); - addBean(_sessionIdListeners); - addBean(_sessionAttributeListeners); + installBean(_sessionManager); + installBean(_cookieConfig); + installBean(_sessionListeners); + installBean(_sessionIdListeners); + installBean(_sessionAttributeListeners); } public SessionManager getSessionManager() diff --git a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java index 9f4eb0705f28..9ca5afcf7986 100644 --- a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java +++ b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java @@ -16,7 +16,6 @@ import java.io.File; import java.net.URI; import java.net.URL; -import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; diff --git a/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/SecurityHandler.java b/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/SecurityHandler.java index ff92704b8345..a3092792b3fd 100644 --- a/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/SecurityHandler.java +++ b/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/SecurityHandler.java @@ -84,7 +84,7 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti protected SecurityHandler() { - addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories)); + installBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories)); } /** diff --git a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletContextHandler.java b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletContextHandler.java index d819163e7c27..a411a03cb313 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletContextHandler.java +++ b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletContextHandler.java @@ -173,7 +173,7 @@ public ServletContextHandler(Container parent, String contextPath, SessionHandle _servletHandler = servletHandler; _objFactory = new DecoratedObjectFactory(); - addBean(_objFactory, true); + installBean(_objFactory, true); // Link the handlers relinkHandlers(); diff --git a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletTester.java b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletTester.java index 90662c0e17c4..7541d0f0f11d 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletTester.java +++ b/jetty-ee9/jetty-ee9-servlet/src/main/java/org/eclipse/jetty/ee9/servlet/ServletTester.java @@ -229,7 +229,7 @@ public ServletTester(String contextPath, int options) { _context = new ServletContextHandler(_server, contextPath, options); _server.setConnectors(new Connector[]{_connector}); - addBean(_server); + installBean(_server); } public ServletContextHandler getContext() diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-loginservice/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-loginservice/pom.xml index fc720cb5947e..fe667eb00d13 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-loginservice/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-loginservice/pom.xml @@ -33,6 +33,11 @@ org.eclipse.jetty.ee9 jetty-ee9-webapp + + commons-codec + commons-codec + test + net.java.dev.jna jna diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-gcloud/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-gcloud/pom.xml index c24b6f30b3ec..e5b0934f3d55 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-gcloud/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-gcloud/pom.xml @@ -12,6 +12,11 @@ ${project.groupId}.sessions.gcloud + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-infinispan/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-infinispan/pom.xml index 8cc7560e0054..8b5d97b6d703 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-infinispan/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-infinispan/pom.xml @@ -21,6 +21,11 @@ ${gson.version} test + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-jdbc/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-jdbc/pom.xml index a802fe564359..692b9ac53302 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-jdbc/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-jdbc/pom.xml @@ -12,6 +12,11 @@ ${project.groupId}.sessions.jdbc + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-memcached/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-memcached/pom.xml index ef847181d8c2..d53b2facac1f 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-memcached/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-memcached/pom.xml @@ -12,6 +12,11 @@ ${project.groupId}.sessions.memcached + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-mongodb/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-mongodb/pom.xml index 6ad26cbe2741..05f41e8a461b 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-mongodb/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-mongodb/pom.xml @@ -12,6 +12,11 @@ ${project.groupId}.sessions.mongo + + commons-codec + commons-codec + test + org.eclipse.jetty jetty-client diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/ClassMatcher.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/ClassMatcher.java index 148486e5ba42..84d05c2a9cf9 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/ClassMatcher.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/ClassMatcher.java @@ -733,6 +733,9 @@ public boolean match(Class clazz) public boolean match(String name, URL url) { + if (url == null) + return false; + // Strip class suffix for name matching if (name.endsWith(".class")) name = name.substring(0, name.length() - 6); diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index c76a53dfe2f7..2873a06f9d95 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -1294,14 +1294,14 @@ protected void startContext() { //resolve the metadata _metadata.resolve(this); - super.startContext(); + startWebapp(); } } @Override protected void stopContext() throws Exception { - super.stopContext(); + stopWebapp(); try { for (int i = _configurations.size(); i-- > 0; ) @@ -1327,6 +1327,24 @@ protected void stopContext() throws Exception } } + /** + * Continue the {@link #startContext()} before calling {@code super.startContext()}. + * @throws Exception If there was a problem starting + */ + protected void startWebapp() throws Exception + { + super.startContext(); + } + + /** + * Continue the {@link #stopContext()} before calling {@code super.stopContext()}. + * @throws Exception If there was a problem stopping + */ + protected void stopWebapp() throws Exception + { + super.stopContext(); + } + @Override public Set setServletSecurity(Dynamic registration, ServletSecurityElement servletSecurityElement) { @@ -1429,7 +1447,7 @@ public URL getResource(String path) throws MalformedURLException // If a WAR file is mounted, or is extracted to a temp directory, // then the first entry of the resource base must be the WAR file. Resource resource = WebAppContext.this.getResource(path); - if (resource == null) + if (Resources.missing(resource)) return null; for (Resource r: resource) diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketContainer.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketContainer.java index 10cb7e004d40..0f94e7ee736a 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketContainer.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee9/websocket/jakarta/common/JakartaWebSocketContainer.java @@ -46,7 +46,7 @@ public JakartaWebSocketContainer(WebSocketComponents components) { this.components = components; addSessionListener(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public abstract Executor getExecutor(); diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java index fcd6b49ad7fd..c5fc52e9797e 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java @@ -85,7 +85,7 @@ public WebSocketClient(HttpClient httpClient) addManaged(coreClient); frameHandlerFactory = new JettyWebSocketFrameHandlerFactory(this, components); sessionListeners.add(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public CompletableFuture connect(Object websocket, URI toUri) throws IOException diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee9/websocket/server/JettyWebSocketServerContainer.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee9/websocket/server/JettyWebSocketServerContainer.java index baae7a52143e..f57c41314736 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee9/websocket/server/JettyWebSocketServerContainer.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-server/src/main/java/org/eclipse/jetty/ee9/websocket/server/JettyWebSocketServerContainer.java @@ -137,10 +137,10 @@ public String toString() this.components = components; this.executor = executor; this.frameHandlerFactory = new JettyServerFrameHandlerFactory(this, components); - addBean(frameHandlerFactory); + installBean(frameHandlerFactory); addSessionListener(sessionTracker); - addBean(sessionTracker); + installBean(sessionTracker); } public void addMapping(String pathSpec, JettyWebSocketCreator creator) diff --git a/jetty-integrations/jetty-gcloud/pom.xml b/jetty-integrations/jetty-gcloud/pom.xml index 0341d624af8a..98a929c4feb6 100644 --- a/jetty-integrations/jetty-gcloud/pom.xml +++ b/jetty-integrations/jetty-gcloud/pom.xml @@ -17,8 +17,8 @@ - 2.17.6 - 3.25.1 + 2.18.4 + 3.25.2 diff --git a/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query/pom.xml b/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query/pom.xml index 33a8a36b4505..29ae3115fe88 100644 --- a/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query/pom.xml +++ b/jetty-integrations/jetty-infinispan/jetty-infinispan-remote-query/pom.xml @@ -48,6 +48,11 @@ org.infinispan infinispan-remote-query-client + + commons-codec + commons-codec + test + org.slf4j slf4j-simple diff --git a/pom.xml b/pom.xml index 60000d702855..7559b5cb6050 100644 --- a/pom.xml +++ b/pom.xml @@ -155,9 +155,9 @@ 1.11.3 4.5.14 4.4.16 - 2.2.4 + 2.2.6 false - 2.2.14 + 2.3.0 2.5.11 9.6 4.2.0 @@ -165,10 +165,10 @@ 3.5.0 1.5 3.2.0 - 10.13.0 - 1.16.0 + 10.14.0 + 1.16.1 3.14.0 - 1.25.0 + 1.26.0 2.15.1 17 17 @@ -179,10 +179,10 @@ 3.33.0 7.0.5 3.0.2 - 1.5.0 - 2.24.1 + 1.6.0 + 2.25.0 4.0.6 - 1.60.1 + 1.62.2 2.10.1 33.0.0-jre 7.0.0 @@ -204,7 +204,7 @@ 2.2.1.Final 3.5.3.Final 1.1 - 0.20.0 + 0.20.1 1.2 2.7 1.0.7 @@ -216,6 +216,7 @@ 1.4 ${project.build.directory}/${jettyHomeZipFileName} jetty-home.zip + 1.3.13 1.37 benchmarks 5.14.0 @@ -227,17 +228,17 @@ concurrent same_thread true - 5.10.0 + 5.10.2 2.0.3 4.3 ${project.build.directory}/local-repo - 2.22.1 - 1.4.14 + 2.23.0 + 1.5.1 10.3.6 - 3.3.2 + 3.3.3 0.13.1 1.1.0 - 3.10.2 + 3.11.0 3.1.0 3.6.0 5.1.9 @@ -249,7 +250,7 @@ 3.1.1 3.9.4 3.4.1 - 3.1.1 + 3.2.0 3.1.0 3.1.1 3.6.0 @@ -260,7 +261,7 @@ 3.1.0 1.9.18 3.3.1 - 3.5.1 + 3.5.2 3.3.0 3.1.2 @@ -297,15 +298,15 @@ src/it/settings.xml 2.0.9 1.3.6 - 4.8.2.0 + 4.8.3.1 false 0 1.8.3 - 1.19.3 + 1.19.6 3.0.0 2.16.2 1.7.0.Final - 2.2.2.Final + 2.3.1.Final 2.4.8 @@ -356,6 +357,11 @@ logback-core ${logback.version} + + com.github.jnr + jffi + ${jffi.version} + com.google.code.findbugs jsr305 diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorExchanger.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorExchanger.java new file mode 100644 index 000000000000..c990b5843d12 --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorExchanger.java @@ -0,0 +1,388 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.thread.jmh; + +import java.io.IOException; +import java.util.concurrent.Exchanger; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.util.ProcessorUtils; +import org.eclipse.jetty.util.VirtualThreads; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.thread.ThreadIdPool; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.ThreadPoolBudget; +import org.eclipse.jetty.util.thread.TryExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

A TryExecutor using pre-allocated/reserved threads from an external Executor.

+ *

Calls to {@link #tryExecute(Runnable)} on ReservedThreadExecutor will either + * succeed with a reserved thread immediately being assigned the task, or fail if + * no reserved thread is available.

+ *

Threads are reserved lazily, with new reserved threads being allocated from the external + * {@link Executor} passed to the constructor. Whenever 1 or more reserved threads have been + * idle for more than {@link #getIdleTimeoutMs()} then one reserved thread will return to + * the external Executor.

+ */ +@ManagedObject("A pool for reserved threads") +public class ReservedThreadExecutorExchanger extends ContainerLifeCycle implements TryExecutor, Dumpable +{ + private static final Logger LOG = LoggerFactory.getLogger(ReservedThreadExecutorExchanger.class); + + private final Executor _executor; + private final ThreadIdPool _threads; + private final AtomicInteger _pending = new AtomicInteger(); + private final int _minSize; + private final int _maxPending; + private ThreadPoolBudget.Lease _lease; + private long _idleTimeoutMs; + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. + */ + public ReservedThreadExecutorExchanger(Executor executor, int capacity) + { + this(executor, capacity, -1); + } + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. + * @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value. + */ + public ReservedThreadExecutorExchanger(Executor executor, int capacity, int minSize) + { + this(executor, capacity, minSize, -1); + } + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads that can be reserved. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool type. + * @param minSize The minimum number of reserve Threads that the algorithm tries to maintain, or -1 for a heuristic value. + * @param maxPending The maximum number of reserved Threads to start, or -1 for no limit. + */ + public ReservedThreadExecutorExchanger(Executor executor, int capacity, int minSize, int maxPending) + { + _executor = executor; + _threads = new ThreadIdPool<>(reservedThreads(executor, capacity)); + _minSize = minSize < 0 ? Math.min(1, _threads.capacity()) : minSize; + if (_minSize > _threads.capacity()) + throw new IllegalArgumentException("minSize larger than capacity"); + _maxPending = maxPending; + if (_maxPending == 0) + throw new IllegalArgumentException("maxPending cannot be 0"); + if (LOG.isDebugEnabled()) + LOG.debug("{}", this); + installBean(_executor); + installBean(_threads); + } + + /** + * Get the heuristic number of reserved threads. + * + * @param executor The executor to use to obtain threads + * @param capacity The number of threads to preallocate, If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool size. + * @return the number of reserved threads that would be used by a ReservedThreadExecutor + * constructed with these arguments. + */ + public static int reservedThreads(Executor executor, int capacity) + { + if (capacity >= 0) + return capacity; + if (VirtualThreads.isUseVirtualThreads(executor)) + return 0; + int cpus = ProcessorUtils.availableProcessors(); + if (executor instanceof ThreadPool.SizedThreadPool) + { + int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads(); + return Math.max(1, Math.min(cpus, threads / 8)); + } + return cpus; + } + + public Executor getExecutor() + { + return _executor; + } + + /** + * @return the maximum number of reserved threads + */ + @ManagedAttribute(value = "max number of reserved threads", readonly = true) + public int getCapacity() + { + return _threads.capacity(); + } + + /** + * @return the number of threads available to {@link #tryExecute(Runnable)} + */ + @ManagedAttribute(value = "available reserved threads", readonly = true) + public int getAvailable() + { + return _threads.size(); + } + + @ManagedAttribute(value = "pending reserved threads (deprecated)", readonly = true) + @Deprecated + public int getPending() + { + return 0; + } + + @ManagedAttribute(value = "idle timeout in ms", readonly = true) + public long getIdleTimeoutMs() + { + return _idleTimeoutMs; + } + + /** + * Set the idle timeout for shrinking the reserved thread pool + * + * @param idleTime Time to wait before shrinking, or 0 for default timeout. + * @param idleTimeUnit Time units for idle timeout + */ + public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit) + { + if (isRunning()) + throw new IllegalStateException(); + _idleTimeoutMs = idleTime <= 0 ? 0 : idleTimeUnit.toMillis(idleTime); + } + + @Override + public void doStart() throws Exception + { + _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, getCapacity()); + super.doStart(); + } + + @Override + public void doStop() throws Exception + { + if (_lease != null) + _lease.close(); + + super.doStop(); + + _threads.removeAll().forEach(ReservedThread::stop); + } + + @Override + public void execute(Runnable task) throws RejectedExecutionException + { + _executor.execute(task); + } + + /** + *

Executes the given task if and only if a reserved thread is available.

+ * + * @param task the task to run + * @return true if and only if a reserved thread was available and has been assigned the task to run. + */ + @Override + public boolean tryExecute(Runnable task) + { + if (task == null) + return false; + + ReservedThread reserved = _threads.take(); + if (reserved != null) + return reserved.wakeup(task); + + startReservedThread(); + + if (LOG.isDebugEnabled()) + LOG.debug("{} tryExecute failed for {}", this, task); + return false; + } + + private void startReservedThread() + { + if (_maxPending > 0 && _pending.incrementAndGet() >= _maxPending) + { + _pending.decrementAndGet(); + return; + } + + try + { + ReservedThread thread = new ReservedThread(); + _executor.execute(thread); + } + catch (Throwable e) + { + if (LOG.isDebugEnabled()) + LOG.debug("ignored", e); + } + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + Dumpable.dumpObjects(out, indent, this); + } + + @Override + public String toString() + { + return String.format("%s@%x{capacity=%d,threads=%s}", + getClass().getSimpleName(), + hashCode(), + getCapacity(), + _threads); + } + + private class ReservedThread implements Runnable + { + private final Exchanger _exchanger = new Exchanger<>(); + private volatile Thread _thread; + + @Override + public void run() + { + _thread = Thread.currentThread(); + boolean pending = true; + try + { + while (true) + { + int slot = _threads.offer(this); + + if (!isRunning() && _threads.remove(this, slot)) + break; + + if (pending) + { + pending = false; + _pending.decrementAndGet(); + } + + if (slot < 0) + // no slot available + return; + + Runnable task = waitForTask(); + while (task == null) + { + if (!isRunning()) + return; + + // shrink if we are already removed or there are other reserved threads. + // There is a small chance multiple threads will shrink below minSize + if (getAvailable() > _minSize && _threads.remove(this, slot)) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} reservedThread shrank {}", ReservedThreadExecutorExchanger.this, this); + return; + } + task = waitForTask(); + } + + try + { + if (LOG.isDebugEnabled()) + LOG.debug("{} reservedThread run {} on {}", ReservedThreadExecutorExchanger.this, task, this); + task.run(); + } + catch (Throwable t) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} task {} failure", ReservedThreadExecutorExchanger.this, task, t); + } + } + } + catch (Throwable t) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} reservedThread {} failure", ReservedThreadExecutorExchanger.this, this, t); + } + finally + { + _thread = null; + // Clear any interrupted status. + if (Thread.interrupted() && LOG.isDebugEnabled()) + LOG.debug("interrupted {}", this); + } + } + + private boolean wakeup(Runnable task) + { + try + { + if (_idleTimeoutMs <= 0) + _exchanger.exchange(task); + else + _exchanger.exchange(task, _idleTimeoutMs, TimeUnit.MILLISECONDS); + return true; + } + catch (Throwable e) + { + if (LOG.isDebugEnabled()) + LOG.debug("exchange failed", e); + } + return false; + } + + private Runnable waitForTask() + { + try + { + if (_idleTimeoutMs <= 0) + return _exchanger.exchange(null); + return _exchanger.exchange(null, _idleTimeoutMs, TimeUnit.MILLISECONDS); + } + catch (Throwable e) + { + if (LOG.isDebugEnabled()) + LOG.debug("wait failed ", e); + } + return null; + } + + private void stop() + { + // If we are stopping, the reserved thread may already have stopped. So just interrupt rather than + // expect an exchange rendezvous. + Thread thread = _thread; + if (thread != null) + thread.interrupt(); + } + + @Override + public String toString() + { + return String.format("%s@%x{thread=%s}", + getClass().getSimpleName(), + hashCode(), + _thread); + } + } +} diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSemaphore.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSemaphore.java new file mode 100644 index 000000000000..e2c6b838caf0 --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSemaphore.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.thread.jmh; + +import java.util.concurrent.Executor; + +import org.eclipse.jetty.util.thread.ReservedThreadExecutor; + +public class ReservedThreadExecutorSemaphore extends ReservedThreadExecutor +{ + public ReservedThreadExecutorSemaphore(Executor executor, int capacity) + { + super(executor, capacity); + } + + public ReservedThreadExecutorSemaphore(Executor executor, int capacity, int minSize) + { + super(executor, capacity, minSize); + } + + public ReservedThreadExecutorSemaphore(Executor executor, int capacity, int minSize, int maxPending) + { + super(executor, capacity, minSize, maxPending); + } +} diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSyncQueue.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSyncQueue.java new file mode 100644 index 000000000000..272ae03d9ced --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadExecutorSyncQueue.java @@ -0,0 +1,435 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.thread.jmh; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.eclipse.jetty.util.AtomicBiInteger; +import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.ProcessorUtils; +import org.eclipse.jetty.util.VirtualThreads; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.component.DumpableCollection; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.ThreadPoolBudget; +import org.eclipse.jetty.util.thread.TryExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.eclipse.jetty.util.AtomicBiInteger.getHi; +import static org.eclipse.jetty.util.AtomicBiInteger.getLo; + +@ManagedObject("A pool for reserved threads") +public class ReservedThreadExecutorSyncQueue extends AbstractLifeCycle implements TryExecutor, Dumpable +{ + private static final Logger LOG = LoggerFactory.getLogger(ReservedThreadExecutorSyncQueue.class); + private static final long DEFAULT_IDLE_TIMEOUT = TimeUnit.MINUTES.toNanos(1); + private static final Runnable STOP = new Runnable() + { + @Override + public void run() + { + } + + @Override + public String toString() + { + return "STOP"; + } + }; + + private final Executor _executor; + private final int _capacity; + private final Set _threads = ConcurrentHashMap.newKeySet(); + private final SynchronousQueue _queue = new SynchronousQueue<>(false); + private final AtomicBiInteger _count = new AtomicBiInteger(); // hi=pending; lo=size; + private final AtomicLong _lastEmptyNanoTime = new AtomicLong(NanoTime.now()); + private ThreadPoolBudget.Lease _lease; + private long _idleTimeNanos = DEFAULT_IDLE_TIMEOUT; + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads to preallocate. If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool size. + */ + public ReservedThreadExecutorSyncQueue(Executor executor, int capacity) + { + _executor = executor; + _capacity = reservedThreads(executor, capacity); + if (LOG.isDebugEnabled()) + LOG.debug("{}", this); + } + + /** + * @param executor The executor to use to obtain threads + * @param capacity The number of threads to preallocate, If less than 0 then capacity + * is calculated based on a heuristic from the number of available processors and + * thread pool size. + * @return the number of reserved threads that would be used by a ReservedThreadExecutor + * constructed with these arguments. + */ + private static int reservedThreads(Executor executor, int capacity) + { + if (capacity >= 0) + return capacity; + if (VirtualThreads.isUseVirtualThreads(executor)) + return 0; + int cpus = ProcessorUtils.availableProcessors(); + if (executor instanceof ThreadPool.SizedThreadPool) + { + int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads(); + return Math.max(1, Math.min(cpus, threads / 8)); + } + return cpus; + } + + public Executor getExecutor() + { + return _executor; + } + + /** + * @return the maximum number of reserved threads + */ + @ManagedAttribute(value = "max number of reserved threads", readonly = true) + public int getCapacity() + { + return _capacity; + } + + /** + * @return the number of threads available to {@link #tryExecute(Runnable)} + */ + @ManagedAttribute(value = "available reserved threads", readonly = true) + public int getAvailable() + { + return _count.getLo(); + } + + @ManagedAttribute(value = "pending reserved threads", readonly = true) + public int getPending() + { + return _count.getHi(); + } + + @ManagedAttribute(value = "idle timeout in ms", readonly = true) + public long getIdleTimeoutMs() + { + return NANOSECONDS.toMillis(_idleTimeNanos); + } + + /** + * Set the idle timeout for shrinking the reserved thread pool + * + * @param idleTime Time to wait before shrinking, or 0 for default timeout. + * @param idleTimeUnit Time units for idle timeout + */ + public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit) + { + if (isRunning()) + throw new IllegalStateException(); + _idleTimeNanos = (idleTime <= 0 || idleTimeUnit == null) ? DEFAULT_IDLE_TIMEOUT : idleTimeUnit.toNanos(idleTime); + } + + @Override + public void doStart() throws Exception + { + _lease = ThreadPoolBudget.leaseFrom(getExecutor(), this, _capacity); + _count.set(0, 0); + super.doStart(); + } + + @Override + public void doStop() throws Exception + { + if (_lease != null) + _lease.close(); + + super.doStop(); + + // Mark this instance as stopped. + int size = _count.getAndSetLo(-1); + + // Offer the STOP task to all waiting reserved threads. + for (int i = 0; i < size; ++i) + { + // Yield to wait for any reserved threads that + // have incremented the size but not yet polled. + Thread.yield(); + _queue.offer(STOP); + } + + // Interrupt any reserved thread missed the offer, + // so they do not wait for the whole idle timeout. + _threads.stream() + .filter(ReservedThread::isReserved) + .map(t -> t._thread) + .filter(Objects::nonNull) + .forEach(Thread::interrupt); + _threads.clear(); + _count.getAndSetHi(0); + } + + @Override + public void execute(Runnable task) throws RejectedExecutionException + { + _executor.execute(task); + } + + /** + *

Executes the given task if and only if a reserved thread is available.

+ * + * @param task the task to run + * @return true if and only if a reserved thread was available and has been assigned the task to run. + */ + @Override + public boolean tryExecute(Runnable task) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} tryExecute {}", this, task); + if (task == null) + return false; + + // Offer will only succeed if there is a reserved thread waiting + boolean offered = _queue.offer(task); + + // If the offer succeeded we need to reduce the size, unless it is set to -1 in the meantime + int size = _count.getLo(); + while (offered && size > 0 && !_count.compareAndSetLo(size, --size)) + size = _count.getLo(); + + // If size is 0 and we are not stopping, start a new reserved thread + if (size == 0 && task != STOP) + startReservedThread(); + + return offered; + } + + private void startReservedThread() + { + while (true) + { + long count = _count.get(); + int pending = getHi(count); + int size = getLo(count); + if (size < 0 || pending + size >= _capacity) + return; + if (size == 0) + _lastEmptyNanoTime.set(NanoTime.now()); + if (!_count.compareAndSet(count, pending + 1, size)) + continue; + + if (LOG.isDebugEnabled()) + LOG.debug("{} startReservedThread p={}", this, pending + 1); + try + { + ReservedThread thread = new ReservedThread(); + _threads.add(thread); + _executor.execute(thread); + } + catch (Throwable e) + { + _count.add(-1, 0); + if (LOG.isDebugEnabled()) + LOG.debug("ignored", e); + } + return; + } + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + Dumpable.dumpObjects(out, indent, this, + new DumpableCollection("threads", + _threads.stream() + .filter(ReservedThread::isReserved) + .collect(Collectors.toList()))); + } + + @Override + public String toString() + { + return String.format("%s@%x{reserved=%d/%d,pending=%d}", + getClass().getSimpleName(), + hashCode(), + _count.getLo(), + _capacity, + _count.getHi()); + } + + private enum State + { + PENDING, + RESERVED, + RUNNING, + IDLE, + STOPPED + } + + private class ReservedThread implements Runnable + { + // The state and thread are kept only for dumping + private volatile State _state = State.PENDING; + private volatile Thread _thread; + + private boolean isReserved() + { + return _state == State.RESERVED; + } + + private Runnable reservedWait() + { + if (LOG.isDebugEnabled()) + LOG.debug("{} waiting {}", this, ReservedThreadExecutorSyncQueue.this); + + // Keep waiting until stopped, tasked or idle + while (_count.getLo() >= 0) + { + try + { + // Always poll at some period as safety to ensure we don't poll forever. + Runnable task = _queue.poll(_idleTimeNanos, NANOSECONDS); + if (LOG.isDebugEnabled()) + LOG.debug("{} task={} {}", this, task, ReservedThreadExecutorSyncQueue.this); + if (task != null) + return task; + + // we have idled out + int size = _count.getLo(); + // decrement size if we have not also been stopped. + while (size > 0) + { + if (_count.compareAndSetLo(size, --size)) + break; + size = _count.getLo(); + } + _state = size >= 0 ? State.IDLE : State.STOPPED; + return STOP; + } + catch (InterruptedException e) + { + if (LOG.isDebugEnabled()) + LOG.debug("ignored", e); + } + } + _state = State.STOPPED; + return STOP; + } + + @Override + public void run() + { + _thread = Thread.currentThread(); + try + { + while (true) + { + long count = _count.get(); + + // reduce pending if this thread was pending + int pending = getHi(count) - (_state == State.PENDING ? 1 : 0); + int size = getLo(count); + + State next; + if (size < 0 || size >= _capacity) + { + // The executor has stopped or this thread is excess to capacity + next = State.STOPPED; + } + else + { + long now = NanoTime.now(); + long lastEmpty = _lastEmptyNanoTime.get(); + if (size > 0 && _idleTimeNanos < NanoTime.elapsed(lastEmpty, now) && _lastEmptyNanoTime.compareAndSet(lastEmpty, now)) + { + // it has been too long since we hit zero reserved threads, so are "busy" idle + next = State.IDLE; + } + else + { + // We will become a reserved thread if we can update the count below. + next = State.RESERVED; + size++; + } + } + + // Update count for pending and size + if (!_count.compareAndSet(count, pending, size)) + continue; + + if (LOG.isDebugEnabled()) + LOG.debug("{} was={} next={} size={}+{} capacity={}", this, _state, next, pending, size, _capacity); + _state = next; + if (next != State.RESERVED) + break; + + // We are reserved whilst we are waiting for an offered _task. + Runnable task = reservedWait(); + + // Is the task the STOP poison pill? + if (task == STOP) + break; + + // Run the task + try + { + _state = State.RUNNING; + task.run(); + } + catch (Throwable e) + { + LOG.warn("Unable to run task", e); + } + finally + { + // Clear any interrupted status. + Thread.interrupted(); + } + } + } + finally + { + if (LOG.isDebugEnabled()) + LOG.debug("{} exited {}", this, ReservedThreadExecutorSyncQueue.this); + _threads.remove(this); + _thread = null; + } + } + + @Override + public String toString() + { + return String.format("%s@%x{%s,thread=%s}", + getClass().getSimpleName(), + hashCode(), + _state, + _thread); + } + } +} diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadPoolBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadPoolBenchmark.java index 0a123ee57a66..61db1d0830ec 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadPoolBenchmark.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ReservedThreadPoolBenchmark.java @@ -13,12 +13,12 @@ package org.eclipse.jetty.util.thread.jmh; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; +import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.util.thread.ReservedThreadExecutor; import org.eclipse.jetty.util.thread.TryExecutor; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -38,23 +38,27 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) -@Warmup(iterations = 3, time = 2000, timeUnit = TimeUnit.MILLISECONDS) -@Measurement(iterations = 3, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 2000, timeUnit = TimeUnit.MILLISECONDS) public class ReservedThreadPoolBenchmark { public enum Type { - RTP + RTE_EXCH, RTE_SEMA, RTE_Q } - @Param({"RTP"}) + @Param({"RTE_EXCH", "RTE_SEMA", "RTE_Q"}) Type type; - @Param({"0", "8", "32"}) + @Param({"16"}) int size; QueuedThreadPool qtp; TryExecutor pool; + LongAdder jobs = new LongAdder(); + LongAdder complete = new LongAdder(); + LongAdder hit = new LongAdder(); + LongAdder miss = new LongAdder(); @Setup // (Level.Iteration) public void buildPool() @@ -62,10 +66,24 @@ public void buildPool() qtp = new QueuedThreadPool(); switch (type) { - case RTP: + case RTE_EXCH: { - ReservedThreadExecutor pool = new ReservedThreadExecutor(qtp, size); - pool.setIdleTimeout(1, TimeUnit.SECONDS); + ReservedThreadExecutorExchanger pool = new ReservedThreadExecutorExchanger(qtp, size); + pool.setIdleTimeout(5, TimeUnit.SECONDS); + this.pool = pool; + break; + } + case RTE_Q: + { + ReservedThreadExecutorSyncQueue pool = new ReservedThreadExecutorSyncQueue(qtp, size); + pool.setIdleTimeout(5, TimeUnit.SECONDS); + this.pool = pool; + break; + } + case RTE_SEMA: + { + ReservedThreadExecutorSemaphore pool = new ReservedThreadExecutorSemaphore(qtp, size); + pool.setIdleTimeout(5, TimeUnit.SECONDS); this.pool = pool; break; } @@ -77,24 +95,39 @@ public void buildPool() @TearDown // (Level.Iteration) public void shutdownPool() { + System.err.println("\nShutdown ..."); + long startSpin = System.nanoTime(); + while (complete.longValue() < jobs.longValue()) + { + if (NanoTime.secondsSince(startSpin) > 5) + { + System.err.printf("FAILED %d < %d\n".formatted(complete.longValue(), jobs.longValue())); + break; + } + Thread.onSpinWait(); + } + System.err.println("Stopping ..."); LifeCycle.stop(pool); LifeCycle.stop(qtp); pool = null; qtp = null; + System.err.println("Stopped"); + long hits = hit.sum(); + System.err.printf("hit:miss = %.1f%% (%d:%d)", 100.0D * hits / (hits + miss.sum()), hits, miss.sum()); } @Benchmark @BenchmarkMode(Mode.Throughput) @Threads(1) - public void testFew() throws Exception + public void test001Threads() throws Exception { doJob(); } @Benchmark @BenchmarkMode(Mode.Throughput) - @Threads(8) - public void testSome() throws Exception + @Threads(32) + public void test032Threads() throws Exception { doJob(); } @@ -102,25 +135,29 @@ public void testSome() throws Exception @Benchmark @BenchmarkMode(Mode.Throughput) @Threads(200) - public void testMany() throws Exception + public void test200Threads() throws Exception { doJob(); } - void doJob() throws Exception + void doJob() { - CountDownLatch latch = new CountDownLatch(1); + jobs.increment(); Runnable task = () -> { - Blackhole.consumeCPU(1); - Thread.yield(); - Blackhole.consumeCPU(1); - latch.countDown(); - Blackhole.consumeCPU(1); + Blackhole.consumeCPU(2); + complete.increment(); }; - if (!pool.tryExecute(task)) + if (pool.tryExecute(task)) + { + hit.increment(); + } + else + { + miss.increment(); qtp.execute(task); - latch.await(); + } + // We don't wait for the job to complete here, as we want to measure the speed of dispatch, not execution latency } public static void main(String[] args) throws RunnerException