From 058dee89b51c897926744169c26b45691e91cf5a Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sat, 22 Feb 2025 17:51:51 +0100 Subject: [PATCH 1/6] Add support of Jetty graceful shutdown --- .../org.restlet.ext.jetty/README.md | 32 ++ .../org.restlet.ext.jetty/pom.xml | 5 + .../restlet/ext/jetty/HttpClientHelper.java | 255 +++++++------ .../restlet/ext/jetty/HttpsServerHelper.java | 7 +- .../restlet/ext/jetty/JettyServerHelper.java | 345 +++++++++++------- .../ext/jetty/internal/JettyHandler.java | 12 +- .../ext/jetty/internal/JettyServerCall.java | 30 +- .../RestletSslContextFactoryClient.java | 9 +- .../RestletSslContextFactoryServer.java | 9 +- .../test/java/org/restlet/ext/jetty/Lock.java | 19 + .../ext/jetty/ShutdownHookTestCase.java | 338 +++++++++++++++++ .../connectors/BaseConnectorsTestCase.java | 126 ++++--- .../ChunkedEncodingPutTestCase.java | 15 +- .../connectors/ChunkedEncodingTestCase.java | 13 +- .../jetty/connectors/GetChunkedTestCase.java | 8 +- .../connectors/GetQueryParamTestCase.java | 10 +- .../ext/jetty/connectors/GetTestCase.java | 17 +- .../ext/jetty/connectors/PostPutTestCase.java | 7 +- .../RemoteClientAddressTestCase.java | 29 +- .../ServerMaxConnectionsTestCase.java | 143 ++++++++ .../connectors/SslBaseConnectorsTestCase.java | 73 ++-- .../SslClientContextGetTestCase.java | 53 +-- .../ext/jetty/connectors/SslGetTestCase.java | 61 ++-- .../restlet/ext/jetty/connectors/dummy.p12 | Bin 0 -> 2726 bytes .../engine/adapter/HttpClientHelper.java | 8 +- .../restlet/engine/adapter/ServerAdapter.java | 2 +- .../engine/connector/ClientHelper.java | 2 +- .../engine/connector/HttpClientHelper.java | 10 +- .../connector/HttpUrlConnectionCall.java | 13 +- .../engine/ssl/DefaultSslContextFactory.java | 22 +- .../java/org/restlet/engine/ssl/SslUtils.java | 5 +- .../security/CertificateAuthenticator.java | 4 +- .../main/java/org/restlet/util/Series.java | 4 +- .../org/restlet/routing/RedirectTestCase.java | 13 +- 34 files changed, 1189 insertions(+), 510 deletions(-) create mode 100644 org.restlet.java/org.restlet.ext.jetty/README.md create mode 100644 org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java create mode 100644 org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java create mode 100644 org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java create mode 100644 org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 diff --git a/org.restlet.java/org.restlet.ext.jetty/README.md b/org.restlet.java/org.restlet.ext.jetty/README.md new file mode 100644 index 0000000000..a430b40dc6 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/README.md @@ -0,0 +1,32 @@ +# Debug Jetty + +## Add full logs + +[Jetty's documentation](https://jetty.org/docs/jetty/12/programming-guide/troubleshooting/logging.html) (the `org.eclipse.jetty:jetty-slf4j-impl`is already added to the `pom.xml`). + +Programmatically: +``` +System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); +``` + +Or add a `jetty-logging.properties`: +``` +org.eclipse.jetty.LEVEL=TRACE +org.eclipse.jetty.client.LEVEL=TRACE +``` + +## Debug using JMX +You need to update the current implementation by hand. + +- [activate JMX](https://jetty.org/docs/jetty/12/programming-guide/arch/jmx.html) +The Jetty server is created in class `JettyServerHelper`. + +``` + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + // Add MBeanContainer to the root component. + jettyServer.addBean(mbeanContainer); +``` + +- to [state tracking](https://jetty.org/docs/jetty/12/programming-guide/troubleshooting/state-tracking.html) + You can use [jconsole](https://docs.oracle.com/javase/8/docs/technotes/guides/management/jconsole.html) to check the state of MBean or just run operations on them. diff --git a/org.restlet.java/org.restlet.ext.jetty/pom.xml b/org.restlet.java/org.restlet.ext.jetty/pom.xml index 3d48e9ad02..cd996044f4 100644 --- a/org.restlet.java/org.restlet.ext.jetty/pom.xml +++ b/org.restlet.java/org.restlet.ext.jetty/pom.xml @@ -45,6 +45,11 @@ jetty-http3-client-transport ${lib-jetty-version} + + org.eclipse.jetty + jetty-slf4j-impl + ${lib-jetty-version} + org.restlet org.restlet diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java index 696591962e..4a552103ce 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java @@ -1,22 +1,15 @@ /** * Copyright 2005-2024 Qlik - * + *

* The contents of this file is subject to the terms of the Apache 2.0 open * source license available at http://www.opensource.org/licenses/apache-2.0 - * + *

* Restlet is a registered trademark of QlikTech International AB. */ package org.restlet.ext.jetty; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.concurrent.Executor; -import java.util.logging.Level; - import org.eclipse.jetty.client.AuthenticationStore; -import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpProxy; @@ -49,6 +42,12 @@ import org.restlet.ext.jetty.internal.JettyClientCall; import org.restlet.ext.jetty.internal.RestletSslContextFactoryClient; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.Executor; +import java.util.logging.Level; + /** * HTTP client connector using the Jetty project. Here is the list of parameters * that are supported. They should be set in the Client's context before it is @@ -201,22 +200,13 @@ * * For the default SSL parameters see the Javadocs of the * {@link DefaultSslContextFactory} class. - * + * * @author Jerome Louvel * @author Tal Liron */ public class HttpClientHelper extends org.restlet.engine.adapter.HttpClientHelper { - public static void main(String[] args) throws Exception { - Client client = new Client(Protocol.HTTP, Protocol.HTTPS); - HttpClientHelper helper = new HttpClientHelper(client); - helper.start(); - HttpClient httpClient = helper.getHttpClient(); - ContentResponse response = httpClient.GET("http://github.io/"); - response.getContentAsString(); - } - /** * The wrapped Jetty HTTP client. */ @@ -239,7 +229,7 @@ public static void main(String[] args) throws Exception { * Constructor. Properties can still be set before the wrapped Jetty HTTP * client is effectively created and configured via the * {@link #createHttpClient()} method. - * + * * @param client The client connector to help. */ public HttpClientHelper(Client client) { @@ -247,14 +237,15 @@ public HttpClientHelper(Client client) { getProtocols().add(Protocol.HTTP); getProtocols().add(Protocol.HTTPS); this.authenticationStore = null; - this.cookieStore = isCookieSupported() ? new HttpCookieStore.Default() + this.cookieStore = isCookieSupported() + ? new HttpCookieStore.Default() : new HttpCookieStore.Empty(); this.executor = null; } /** * Creates a low-level HTTP client call from a high-level uniform call. - * + * * @param request The high-level request. * @return A low-level HTTP client call. */ @@ -275,7 +266,7 @@ public ClientCall create(Request request) { /** * Creates a Jetty HTTP client. - * + * * @return A new HTTP client. */ protected HttpClient createHttpClient() { @@ -294,50 +285,50 @@ protected HttpClient createHttpClient() { HTTP3Client http3Client = null; switch (getHttpClientTransportMode()) { - case "HTTP2": - http2Client = new HTTP2Client(); - HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2( - http2Client); - http2Transport.setUseALPN(true); - httpTransport = http2Transport; - break; - - case "HTTP3": - ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(clientQuicConfig); - http3Client.getQuicConfiguration() - .setSessionRecvWindow(64 * 1024 * 1024); - httpTransport = new HttpClientTransportOverHTTP3(http3Client); - break; - - case "DYNAMIC": - ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; - - http2Client = new HTTP2Client(); - ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( - http2Client); - - ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(quicConfiguration); - ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3( - http3Client); - - HttpClientTransportDynamic httpDynamicTransport = new HttpClientTransportDynamic( - new ClientConnector(), http1, http2, http3); - httpTransport = httpDynamicTransport; - break; - - case "HTTP11": - default: - httpTransport = new HttpClientTransportOverHTTP(); - break; + case "HTTP2": + http2Client = new HTTP2Client(); + HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2( + http2Client); + http2Transport.setUseALPN(true); + httpTransport = http2Transport; + break; + + case "HTTP3": + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration( + sslContextFactory, null); + http3Client = new HTTP3Client(clientQuicConfig); + http3Client.getQuicConfiguration() + .setSessionRecvWindow(64 * 1024 * 1024); + httpTransport = new HttpClientTransportOverHTTP3(http3Client); + break; + + case "DYNAMIC": + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + http2Client = new HTTP2Client(); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( + http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration( + sslContextFactory, null); + http3Client = new HTTP3Client(quicConfiguration); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3( + http3Client); + + HttpClientTransportDynamic httpDynamicTransport = new HttpClientTransportDynamic( + new ClientConnector(), http1, http2, http3); + httpTransport = httpDynamicTransport; + break; + + case "HTTP11": + default: + httpTransport = new HttpClientTransportOverHTTP(); + break; } HttpClient httpClient = new HttpClient(httpTransport); httpClient.setAddressResolutionTimeout(getAddressResolutionTimeout()); - if(getAuthenticationStore() != null) { + if (getAuthenticationStore() != null) { httpClient.setAuthenticationStore(getAuthenticationStore()); } httpClient.setBindAddress(getBindAddress()); @@ -349,18 +340,18 @@ protected HttpClient createHttpClient() { switch (getHttpComplianceMode()) { - case "RFC7230": - httpClient.setHttpCompliance(HttpCompliance.RFC7230); - break; - case "RFC7230_LEGACY": - httpClient.setHttpCompliance(HttpCompliance.RFC7230_LEGACY); - break; - case "RFC2616": - httpClient.setHttpCompliance(HttpCompliance.RFC2616); - break; - case "RFC2616_LEGACY": - httpClient.setHttpCompliance(HttpCompliance.RFC2616_LEGACY); - break; + case "RFC7230": + httpClient.setHttpCompliance(HttpCompliance.RFC7230); + break; + case "RFC7230_LEGACY": + httpClient.setHttpCompliance(HttpCompliance.RFC7230_LEGACY); + break; + case "RFC2616": + httpClient.setHttpCompliance(HttpCompliance.RFC2616); + break; + case "RFC2616_LEGACY": + httpClient.setHttpCompliance(HttpCompliance.RFC2616_LEGACY); + break; } httpClient.setHttpCookieStore(getCookieStore()); @@ -396,7 +387,7 @@ protected HttpClient createHttpClient() { /** * The timeout in milliseconds for the DNS resolution of host addresses. * Defaults to 15000. - * + * * @return The address resolution timeout. */ public long getAddressResolutionTimeout() { @@ -406,16 +397,26 @@ public long getAddressResolutionTimeout() { /** * Returns the wrapped Jetty authentication store. - * + * * @return The wrapped Jetty authentication store. */ public AuthenticationStore getAuthenticationStore() { return authenticationStore; } + /** + * Sets the wrapped Jetty authentication store. + * + * @param authenticationStore The wrapped Jetty authentication store. + */ + public void setAuthenticationStore( + AuthenticationStore authenticationStore) { + this.authenticationStore = authenticationStore; + } + /** * The address to bind socket channels to. Default to null. - * + * * @return The bind address or null. */ public SocketAddress getBindAddress() { @@ -432,7 +433,7 @@ public SocketAddress getBindAddress() { /** * The max time in milliseconds a connection can take to connect to * destinations. Defaults to 15000. - * + * * @return The connect timeout. */ public long getConnectTimeout() { @@ -442,17 +443,26 @@ public long getConnectTimeout() { /** * Returns the wrapped Jetty cookie store. - * + * * @return The wrapped Jetty cookie store. */ public HttpCookieStore getCookieStore() { return this.cookieStore; } + /** + * Sets the wrapped Jetty cookie store. + * + * @param cookieStore The wrapped Jetty cookie store. + */ + public void setCookieStore(HttpCookieStore cookieStore) { + this.cookieStore = cookieStore; + } + /** * The timeout in milliseconds for idle destinations to be removed. Defaults * to 15000. - * + * * @return The address resolution timeout. */ public long getDestinationIdleTimeout() { @@ -461,18 +471,27 @@ public long getDestinationIdleTimeout() { } /** - * Returns the executor. By default returns an instance of + * Returns the executor. By default, returns an instance of * {@link QueuedThreadPool}. - * + * * @return Returns the executor. */ public Executor getExecutor() { return this.executor; } + /** + * Sets the executor. + * + * @param executor The executor. + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + /** * Returns the wrapped Jetty HTTP client. - * + * * @return The wrapped Jetty HTTP client. */ public HttpClient getHttpClient() { @@ -482,8 +501,8 @@ public HttpClient getHttpClient() { /** * Returns the HTTP compliance mode among the following options: "RFC7230", * "RFC2616", "LEGACY", "RFC7230_LEGACY". See {@link HttpCompliance}. - * Defaults to "RFC7230". - * + * Default to "RFC7230". + * * @return The HTTP compliance mode. */ public String getHttpComplianceMode() { @@ -495,7 +514,7 @@ public String getHttpComplianceMode() { * Returns the HTTP client transport mode among the following options: * "HTTP11", "HTTP2", "HTTP3", "DYNAMIC. See {@link HttpClientTransport}. * Defaults to "HTTP11". - * + * * @return The HTTP client transport mode. */ public String getHttpClientTransportMode() { @@ -506,7 +525,7 @@ public String getHttpClientTransportMode() { /** * The max time in milliseconds a connection can be idle (that is, without * traffic of bytes in either direction). Defaults to 60000. - * + * * @return The idle timeout. */ public long getIdleTimeout() { @@ -524,7 +543,7 @@ public long getIdleTimeout() { * load test), and it is recommended to set this value to a high value (at * least as much as the threads present in the {@link #getExecutor() * executor}). - * + * * @return The maximum connections per destination. */ public int getMaxConnectionsPerDestination() { @@ -534,7 +553,7 @@ public int getMaxConnectionsPerDestination() { /** * The max number of HTTP redirects that are followed. Defaults to 8. - * + * * @return The maximum redirects. */ public int getMaxRedirects() { @@ -553,7 +572,7 @@ public int getMaxRedirects() { * If this client is used for load testing, it is common to have this * parameter set to a high value, although this may impact latency (requests * sit in the queue for a long time before being sent). - * + * * @return The maximum requests queues per destination. */ public int getMaxRequestsQueuedPerDestination() { @@ -563,8 +582,8 @@ public int getMaxRequestsQueuedPerDestination() { /** * Returns the max size in bytes of the response headers. Default is -1 - * which is unlimited. - * + * that is unlimited. + * * @return the max size in bytes of the response headers. */ public int getMaxResponseHeadersSize() { @@ -574,7 +593,7 @@ public int getMaxResponseHeadersSize() { /** * Returns the host name of the HTTP proxy, if specified. - * + * * @return the host name of the HTTP proxy, if specified. */ public String getProxyHost() { @@ -584,7 +603,7 @@ public String getProxyHost() { /** * Returns the port of the HTTP proxy, if specified, 3128 otherwise. - * + * * @return the port of the HTTP proxy. */ public int getProxyPort() { @@ -594,7 +613,7 @@ public int getProxyPort() { /** * The size in bytes of the buffer used to write requests. Defaults to 4096. - * + * * @return The request buffer size. */ public int getRequestBufferSize() { @@ -605,7 +624,7 @@ public int getRequestBufferSize() { /** * The size in bytes of the buffer used to read responses. Defaults to * 16384. - * + * * @return The response buffer size. */ public int getResponseBufferSize() { @@ -616,7 +635,7 @@ public int getResponseBufferSize() { /** * The scheduler. Defaults to null. When null, creates a new instance of * {@link ScheduledExecutorScheduler}. - * + * * @return The scheduler. */ public Scheduler getScheduler() { @@ -625,8 +644,8 @@ public Scheduler getScheduler() { /** * The "User-Agent" HTTP header string. When null, uses the Jetty default. - * Defaults to null. - * + * Default to null. + * * @return The user agent field or null. */ public String getUserAgentField() { @@ -636,7 +655,7 @@ public String getUserAgentField() { /** * Indicates whether the connect operation is blocking. See * {@link HttpClient#isConnectBlocking()}. - * + * * @return True if the connect operation is blocking. */ public boolean isConnectBlocking() { @@ -647,7 +666,7 @@ public boolean isConnectBlocking() { /** * Whether to support cookies, storing and automatically sending them back. * Defaults to false. - * + * * @return Whether to support cookies. */ public boolean isCookieSupported() { @@ -657,7 +676,7 @@ public boolean isCookieSupported() { /** * Whether to follow HTTP redirects. Defaults to true. - * + * * @return Whether to follow redirects. */ public boolean isFollowRedirects() { @@ -686,7 +705,7 @@ public boolean isFollowRedirects() { * When not enforced, a "begin" event of a second request may happen before * the "complete" event of a first request and allow for better usage of * connections. - * + * * @return Whether request events must be strictly ordered. */ public boolean isStrictEventOrdering() { @@ -694,34 +713,6 @@ public boolean isStrictEventOrdering() { .getFirstValue("strictEventOrdering", "false")); } - /** - * Sets the wrapped Jetty authentication store. - * - * @param authenticationStore The wrapped Jetty authentication store. - */ - public void setAuthenticationStore( - AuthenticationStore authenticationStore) { - this.authenticationStore = authenticationStore; - } - - /** - * Sets the wrapped Jetty cookie store. - * - * @param cookieStore The wrapped Jetty cookie store. - */ - public void setCookieStore(HttpCookieStore cookieStore) { - this.cookieStore = cookieStore; - } - - /** - * Sets the executor. - * - * @param executor The executor. - */ - public void setExecutor(Executor executor) { - this.executor = executor; - } - @Override public void start() throws Exception { super.start(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java index e0f0416061..4e4fb78749 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java @@ -66,16 +66,19 @@ public HttpsServerHelper(Server server) { @Override protected ConnectionFactory[] createConnectionFactories( HttpConfiguration configuration) { + ConnectionFactory[] result; + try { SslContextFactory.Server sslContextFactory = new RestletSslContextFactoryServer( org.restlet.engine.ssl.SslUtils.getSslContextFactory(this)); - return AbstractConnectionFactory.getFactories(sslContextFactory, + result = AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory(configuration)); } catch (Exception e) { + result = null; getLogger().log(Level.WARNING, "Unable to create the Jetty SSL context factory", e); } - return null; + return result; } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java index dcacf062e8..31fbf2be82 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java @@ -9,19 +9,10 @@ package org.restlet.ext.jetty; -import java.net.Socket; -import java.util.Arrays; -import java.util.concurrent.Executor; - import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.LowResourceMonitor; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; @@ -30,6 +21,10 @@ import org.restlet.Server; import org.restlet.ext.jetty.internal.JettyServerCall; +import java.net.Socket; +import java.util.Arrays; +import java.util.concurrent.Executor; + /** * Abstract Jetty web server connector. Here is the list of parameters that are * supported. They should be set in the Server's context before it is started: @@ -77,15 +72,14 @@ * connector.acceptors * int * -1 - * Connector acceptor thread count; when -1, Jetty will default to - * {@link Runtime#availableProcessors()} / 2, with a minimum of 1 + * Connector acceptor thread count; when -1, Jetty will default to 1 * * * connector.selectors * int * -1 - * Connector selector thread count; when -1, Jetty will default to - * {@link Runtime#availableProcessors()} + * Connector selector thread count; When less or equal than 0, + * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size.} * * * connector.acceptQueueSize @@ -152,41 +146,66 @@ * lowResource.period * int * 1000 - * Low resource monitor period in milliseconds; when 0, low resource + * Low-resource monitor period in milliseconds; when 0, low-resource * monitoring is disabled * * * lowResource.threads * boolean * true - * Low resource monitor, whether to check if we're low on threads + * Low-resource monitor, whether to check if we're low on threads * * * lowResource.maxMemory * int * 0 - * Low resource monitor max memory in bytes; when 0, the check disabled; + * Low-resource monitor max memory in bytes; when 0, the check disabled; * memory used is calculated as (totalMemory-freeMemory) * * * lowResource.maxConnections * int * 0 - * Low resource monitor max connections; when 0, the check is disabled + * Low-resource monitor max connections; when 0, the check is disabled. Deprecated, see server.maxConnections * * * lowResource.idleTimeout * int * 1000 - * Low resource monitor idle timeout in milliseconds; applied to EndPoints - * when in the low resources state + * Low-resource monitor idle timeout in milliseconds; applied to EndPoints + * when in the low-resources state * * * lowResource.stopTimeout * long * 30000 - * Low resource monitor stop timeout in milliseconds; the maximum time - * allowed for the service to shutdown + * Low-resource monitor stop timeout in milliseconds; the maximum time + * allowed for the service to shutdown. Deprecated, use shutdown.timeout instead + * + * + * server.maxConnections + * int + * 0 + * Server max connections; when 0, there is no limit + * + * + * server.maxConnections.idleTimeout + * long + * 0 + * The endpoint idle timeout in milliseconds to apply when the connection limit is reached; when 0, there is no idle timeout. + * + * + * shutdown.gracefully + * boolean + * true + * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending + * requests to end before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted. + * + * + * shutdown.timeout + * long + * 30000 + * Server shutdown timeout in milliseconds. Defaults to 30000. * * * @@ -199,45 +218,6 @@ public abstract class JettyServerHelper extends org.restlet.engine.adapter.HttpServerHelper { - /** - * Jetty server wrapped by a parent Restlet HTTP server connector. - * - * @author Jerome Louvel - * @author Tal Liron - */ - private static class WrappedServer extends org.eclipse.jetty.server.Server { - private final JettyServerHelper serverHelper; - - /** - * Constructor. - * - * @param serverHelper The Jetty HTTP server. - * @param threadPool The thread pool. - */ - public WrappedServer(JettyServerHelper serverHelper, - ThreadPool threadPool) { - super(threadPool); - this.serverHelper = serverHelper; - } - - /** - * Handler method converting a Jetty call into a Restlet Call. - * - * @param request The Jetty request to handle. - * @param response The Jetty response to handle. - * @param callback The Jetty callback to use if needed. - * @return True if processing was successful. - */ - @Override - public boolean handle(Request request, Response response, - Callback callback) throws Exception { - this.serverHelper - .handle(new JettyServerCall(this.serverHelper.getHelped(), - request, response, callback)); - return true; - } - } - /** The wrapped Jetty server. */ private volatile org.eclipse.jetty.server.Server wrappedServer; @@ -300,44 +280,42 @@ private Connector createConnector(org.eclipse.jetty.server.Server server) { connectionFactories); final String address = getHelped().getAddress(); - if (address != null) + if (address != null) { connector.setHost(address); + } connector.setPort(getHelped().getPort()); connector.setAcceptQueueSize(getConnectorAcceptQueueSize()); connector.setIdleTimeout(getConnectorIdleTimeout()); -// connector.setSoLingerTime(getConnectorSoLingerTime()); -// connector.setStopTimeout(getConnectorStopTimeout()); + connector.setShutdownIdleTimeout(getShutdownTimeout()); + // connector.setSoLingerTime(getConnectorSoLingerTime()); return connector; } /** - * Creates a Jetty low resource monitor. + * Creates a Jetty low-resource monitor. * * @param server A Jetty server. - * @return A Jetty low resource monitor or null. + * @return A Jetty low-resource monitor or null. */ private LowResourceMonitor createLowResourceMonitor( org.eclipse.jetty.server.Server server) { + final LowResourceMonitor result; + final int period = getLowResourceMonitorPeriod(); if (period > 0) { - final LowResourceMonitor lowResourceMonitor = new LowResourceMonitor( - server); - lowResourceMonitor.setMonitoredConnectors( - Arrays.asList(server.getConnectors())); - lowResourceMonitor.setPeriod(period); - lowResourceMonitor - .setMonitorThreads(getLowResourceMonitorThreads()); - lowResourceMonitor.setMaxMemory(getLowResourceMonitorMaxMemory()); -// lowResourceMonitor.setMaxConnections(getLowResourceMonitorMaxConnections()); - lowResourceMonitor.setLowResourcesIdleTimeout( - getLowResourceMonitorIdleTimeout()); -// lowResourceMonitor.setStopTimeout(getLowResourceMonitorStopTimeout()); - server.addBean(lowResourceMonitor); - return lowResourceMonitor; + result = new LowResourceMonitor(server); + result.setMonitoredConnectors(Arrays.asList(server.getConnectors())); + result.setPeriod(period); + result.setMonitorThreads(getLowResourceMonitorThreads()); + result.setMaxMemory(getLowResourceMonitorMaxMemory()); + result.setLowResourcesIdleTimeout(getLowResourceMonitorIdleTimeout()); + } else { + result = null; } - return null; + + return result; } /** @@ -350,18 +328,60 @@ private org.eclipse.jetty.server.Server createServer() { final ThreadPool threadPool = createThreadPool(); // Server - final org.eclipse.jetty.server.Server server = new WrappedServer(this, - threadPool); + final org.eclipse.jetty.server.Server jettyServer = new org.eclipse.jetty.server.Server(threadPool); - // Connector - final Connector connector = createConnector(server); - server.addConnector(connector); + int serverMaxConnections = getServerMaxConnections(); + if (serverMaxConnections > 0) { + ConnectionLimit connectionLimit = new ConnectionLimit(serverMaxConnections, jettyServer); + connectionLimit.setIdleTimeout(getServerMaxConnectionsIdleTimeout()); + jettyServer.addBean(connectionLimit); + } + + jettyServer.setStopAtShutdown(getShutdownGracefully()); + jettyServer.setStopTimeout(getShutdownTimeout()); - // Low resource monitor (must be created after connectors have been - // added) - createLowResourceMonitor(server); + jettyServer.setHandler(createJettyHandler()); - return server; + // Connector + final Connector connector = createConnector(jettyServer); + jettyServer.addConnector(connector); + + // Low-resource monitor (must be created after connectors have been added) + LowResourceMonitor lowResourceMonitor = createLowResourceMonitor(jettyServer); + jettyServer.addBean(lowResourceMonitor); + + return jettyServer; + } + + /** + * Creates the Jetty handler that wraps the {@link JettyServerHelper}. + * + * @return the Jetty handler that wraps the {@link JettyServerHelper}. + */ + private Handler.Abstract createJettyHandler() { + final Handler.Abstract result; + + final JettyServerHelper jettyServerHelper = this; + Handler.Abstract jettyServerHelperWrapperHandler = new Handler.Abstract() { + @Override + public boolean handle(Request request, Response response, Callback callback) { + JettyServerCall httpCall = new JettyServerCall(jettyServerHelper.getHelped(), + request, response, callback); + jettyServerHelper.handle(httpCall); + return true; // Indicates that the request is accepted + }; + }; + + if (getShutdownGracefully()) { + // StatisticsHandler for graceful shutdown + final StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.setHandler(jettyServerHelperWrapperHandler); + result = statisticsHandler; + } else { + result = jettyServerHelperWrapperHandler; + } + + return result; } /** @@ -381,8 +401,7 @@ private ThreadPool createThreadPool() { /** * Connector acceptor thread count. Defaults to -1. When -1, Jetty will - * default to {@link Runtime#availableProcessors()} / 2, with a minimum of - * 1. + * default to 1. * * @return Connector acceptor thread count. */ @@ -392,9 +411,10 @@ public int getConnectorAcceptors() { } /** - * Connector accept queue size. Defaults to 0. + * Connector "accept" queue size. + * Defaults to 0. *

- * Also known as accept backlog. + * Also known as "accept" backlog. * * @return Connector accept queue size. */ @@ -450,8 +470,10 @@ public Scheduler getConnectorScheduler() { } /** - * Connector selector thread count. Defaults to -1. When 0, Jetty will - * default to {@link Runtime#availableProcessors()}. + * Connector selector thread count. + * Defaults to -1. + * When less or equal than 0, + * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size. * * @return Connector acceptor thread count. */ @@ -476,10 +498,12 @@ public int getConnectorSoLingerTime() { /** * Connector stop timeout in milliseconds. Defaults to 30000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * * @return Connector stop timeout. + * @deprecated cf {@link #getShutdownTimeout()} */ + @Deprecated public int getConnectorStopTimeout() { return Integer.parseInt(getHelpedParameters() .getFirstValue("connector.stopTimeout", "30000")); @@ -499,7 +523,7 @@ public int getHttpHeaderCacheSize() { * HTTP output buffer size in bytes. Defaults to 32*1024. *

* A larger buffer can improve performance by allowing a content producer to - * run without blocking, however larger buffers consume more memory and may + * run without blocking, however, larger buffers consume more memory and may * induce some latency before a client starts processing the content. * * @return HTTP output buffer size. @@ -527,7 +551,7 @@ public int getHttpRequestHeaderSize() { * HTTP response header size in bytes. Defaults to 8*1024. *

* Larger headers will allow for more and/or larger cookies and longer HTTP - * headers (e.g. for redirection). However, larger headers will also consume + * headers (e.g., for redirection). However, larger headers will also consume * more memory. * * @return HTTP response header size. @@ -538,11 +562,12 @@ public int getHttpResponseHeaderSize() { } /** - * Low resource monitor idle timeout in milliseconds. Defaults to 1000. + * Low-resource monitor idle timeout in milliseconds. + * Defaults to 1000. *

- * Applied to EndPoints when in the low resources state. + * Applied to EndPoints when in the low-resources state. * - * @return Low resource monitor idle timeout. + * @return Low-resource monitor idle timeout. */ public int getLowResourceMonitorIdleTimeout() { return Integer.parseInt(getHelpedParameters() @@ -550,23 +575,27 @@ public int getLowResourceMonitorIdleTimeout() { } /** - * Low resource monitor max connections. Defaults to 0. When 0, the check is - * disabled. + * Low-resource monitor max connections. + * Defaults to 0. + * When 0, the check is disabled. * - * @return Low resource monitor max connections. + * @return Low-resource monitor max connections. + * @deprecated cf {@link #getServerMaxConnections()} */ + @Deprecated public int getLowResourceMonitorMaxConnections() { return Integer.parseInt(getHelpedParameters() .getFirstValue("lowResource.maxConnections", "0")); } /** - * Low resource monitor max memory in bytes. Defaults to 0. When 0, the - * check disabled. + * Low-resource monitor max memory in bytes. + * Defaults to 0. + * When 0, the check is disabled. *

* Memory used is calculated as (totalMemory-freeMemory). * - * @return Low resource monitor max memory. + * @return Low-resource monitor max memory. */ public long getLowResourceMonitorMaxMemory() { return Long.parseLong(getHelpedParameters() @@ -574,10 +603,11 @@ public long getLowResourceMonitorMaxMemory() { } /** - * Low resource monitor period in milliseconds. Defaults to 1000. When 0, - * low resource monitoring is disabled. + * Low-resource monitor period in milliseconds. + * Defaults to 1000. + * When 0, low-resource monitoring is disabled. * - * @return Low resource monitor period. + * @return Low-resource monitor period. */ public int getLowResourceMonitorPeriod() { return Integer.parseInt(getHelpedParameters() @@ -585,28 +615,99 @@ public int getLowResourceMonitorPeriod() { } /** - * Low resource monitor stop timeout in milliseconds. Defaults to 30000. + * Low-resource monitor stop timeout in milliseconds. + * Defaults to 30000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * - * @return Low resource monitor stop timeout. + * @return Low-resource monitor stop timeout. + * @deprecated cf {@link #getShutdownTimeout()} */ + @Deprecated public long getLowResourceMonitorStopTimeout() { return Long.parseLong(getHelpedParameters() .getFirstValue("lowResource.stopTimeout", "30000")); } /** - * Low resource monitor, whether to check if we're low on threads. Defaults + * Low-resource monitor, whether to check if we're low on threads. Defaults * to true. * - * @return Low resource monitor threads. + * @return Low-resource monitor threads. */ public boolean getLowResourceMonitorThreads() { return Boolean.parseBoolean(getHelpedParameters() .getFirstValue("lowResource.threads", "true")); } + /** + * Server max connections. + * Defaults to 0. + * When 0, the check is disabled. + * + * @return Low-resource monitor max connections. + */ + public int getServerMaxConnections() { + final int result; + + final String serverMaxConnectionsValue = getHelpedParameters().getFirstValue("server.maxConnections"); + + if (serverMaxConnectionsValue == null) { + result = getLowResourceMonitorMaxConnections(); + } else { + result = Integer.parseInt(serverMaxConnectionsValue); + } + + return result; + } + + /** + * The endpoint idle timeout in milliseconds to apply when the connection limit is reached. + * Defaults to 0. + * When 0, there is no idle timeout. + *

+ * The maximum time allowed for the endpoint to close when the connection limit is reached. + * + * @return The endpoint idle timeout in milliseconds to apply when the connection limit is reached. + */ + public long getServerMaxConnectionsIdleTimeout() { + return Long.parseLong(getHelpedParameters() + .getFirstValue("server.maxConnections.idleTimeout", "0")); + } + + /** + * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending + * requests to end before shutting down. + * Otherwise, incoming requests are not blocked and all requests are aborted. + * Defaults to true. + * @return True if upon JVM shutdown, the Jetty server will block incoming requests and wait for pending requests to + * end before shutting down. + * Otherwise, incoming requests are not blocked and all requests are aborted. + */ + public boolean getShutdownGracefully() { + return Boolean.parseBoolean(getHelpedParameters().getFirstValue( + "shutdown.gracefully", "true")); + } + + /** + * Server shutdown timeout in milliseconds. Defaults to 30000. + * + * @return Server shutdown timeout. + */ + public long getShutdownTimeout() { + final long result; + + final String shutdownTimeoutValue = getHelpedParameters().getFirstValue("shutdown.timeout"); + + if (shutdownTimeoutValue == null) { + result = getLowResourceMonitorStopTimeout(); + } else { + result = Long.parseLong(shutdownTimeoutValue); + } + + return result; + } + /** * Thread pool idle timeout in milliseconds. Defaults to 60000. *

@@ -642,7 +743,7 @@ public int getThreadPoolMinThreads() { /** * Thread pool stop timeout in milliseconds. Defaults to 5000. *

- * The maximum time allowed for the service to shutdown. + * The maximum time allowed for the service to shut down. * * @return Thread pool stop timeout. */ @@ -668,8 +769,9 @@ public int getThreadPoolThreadsPriority() { * @return The wrapped Jetty server. */ protected org.eclipse.jetty.server.Server getWrappedServer() { - if (this.wrappedServer == null) + if (this.wrappedServer == null) { this.wrappedServer = createServer(); + } return this.wrappedServer; } @@ -678,8 +780,7 @@ protected org.eclipse.jetty.server.Server getWrappedServer() { * * @param wrappedServer The wrapped Jetty server. */ - protected void setWrappedServer( - org.eclipse.jetty.server.Server wrappedServer) { + protected void setWrappedServer(org.eclipse.jetty.server.Server wrappedServer) { this.wrappedServer = wrappedServer; } @@ -693,7 +794,7 @@ public void start() throws Exception { try { server.start(); } catch (Exception e) { - // Make sure that all resources are released, otherwise threadpool + // Make sure that all resources are released, otherwise thread-pool // may still be running. server.stop(); throw e; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java index 5e41ad3382..ecbe2033d0 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyHandler.java @@ -20,7 +20,7 @@ /** * Jetty handler that knows how to convert Jetty calls into Restlet calls. This - * handler isn't a full server, if you use it you need to manually setup the + * handler isn't a full server, if you use it, you need to manually set up the * Jetty server connector and add this handler to a Jetty server. * * @author Valdis Rigdon @@ -48,10 +48,11 @@ public JettyHandler(Server server) { * @param secure Indicates if the server supports HTTP or HTTPS. */ public JettyHandler(Server server, boolean secure) { - if (secure) + if (secure) { this.helper = new HttpsServerHelper(server); - else + } else { this.helper = new HttpServerHelper(server); + } } @Override @@ -77,8 +78,9 @@ protected void doStop() throws Exception { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - this.helper.handle(new JettyServerCall(this.helper.getHelped(), request, - response, callback)); + JettyServerCall httpCall = new JettyServerCall(this.helper.getHelped(), + request, response, callback); + this.helper.handle(httpCall); return true; } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java index d95e9bc465..8d81593a42 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/JettyServerCall.java @@ -9,13 +9,6 @@ package org.restlet.ext.jetty.internal; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.cert.Certificate; -import java.util.Arrays; -import java.util.List; - import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; @@ -29,6 +22,13 @@ import org.restlet.engine.adapter.ServerCall; import org.restlet.util.Series; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.List; + /** * Call that is used by the Jetty HTTP server connectors. * @@ -93,12 +93,20 @@ public Callback getCallback() { @Override public List getCertificates() { + final List result; + if (getEndPoint() instanceof SslEndPoint sslEndPoint) { - return Arrays.asList(sslEndPoint.getSslSessionData() - .peerCertificates()); - } else { - return null; + if (sslEndPoint.getSslSessionData() != null + && sslEndPoint.getSslSessionData().peerCertificates() != null) { + result = Arrays.asList(sslEndPoint.getSslSessionData().peerCertificates()); + } else { + result = null; + } + } else { + result = null; } + + return result; } /** diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java index 6b940e8a00..0aa811c5aa 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryClient.java @@ -52,11 +52,10 @@ public SSLEngine newSSLEngine(String host, int port) { @Override public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { - SSLServerSocketFactory factory = getSslContext() - .getServerSocketFactory(); - return (SSLServerSocket) ((host == null) ? factory.createServerSocket( - port, backlog) : factory.createServerSocket(port, backlog, - InetAddress.getByName(host))); + SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); + return (SSLServerSocket) ((host == null) + ? factory.createServerSocket(port, backlog) : + factory.createServerSocket(port, backlog, InetAddress.getByName(host))); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java index 5a70a8e46d..589a41846b 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java @@ -52,11 +52,10 @@ public SSLEngine newSSLEngine(String host, int port) { @Override public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { - SSLServerSocketFactory factory = getSslContext() - .getServerSocketFactory(); - return (SSLServerSocket) ((host == null) ? factory.createServerSocket( - port, backlog) : factory.createServerSocket(port, backlog, - InetAddress.getByName(host))); + SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); + return (SSLServerSocket) ((host == null) + ? factory.createServerSocket(port, backlog) + : factory.createServerSocket(port, backlog, InetAddress.getByName(host))); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java new file mode 100644 index 0000000000..4aab0d1143 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java @@ -0,0 +1,19 @@ +package org.restlet.ext.jetty; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class Lock { + private final CountDownLatch lock = new CountDownLatch(1); + + public void unlock() { + lock.countDown(); + } + + public boolean awaitForUnlockingFor(final Duration waitTime) throws InterruptedException { + final long waitTimeInMs = waitTime.toMillis(); + return lock.await(waitTimeInMs, MILLISECONDS); + } +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java new file mode 100644 index 0000000000..de6c170f33 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java @@ -0,0 +1,338 @@ +/** + * Copyright 2005-2014 Restlet + * + * The contents of this file are subject to the terms of one of the following + * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can + * select the license that you prefer but you may not use this file except in + * compliance with one of these Licenses. + * + * You can obtain a copy of the Apache 2.0 license at + * http://www.opensource.org/licenses/apache-2.0 + * + * You can obtain a copy of the EPL 1.0 license at + * http://www.opensource.org/licenses/eclipse-1.0 + * + * See the Licenses for the specific language governing permissions and + * limitations under the Licenses. + * + * Alternatively, you can obtain a royalty free commercial license with less + * limitations, transferable or non-transferable, directly at + * http://restlet.com/products/restlet-framework + * + * Restlet is a registered trademark of Restlet S.A.S. + */ + +package org.restlet.ext.jetty; + +import org.junit.jupiter.api.Test; +import org.restlet.*; +import org.restlet.data.MediaType; +import org.restlet.data.Protocol; +import org.restlet.data.Status; +import org.restlet.engine.Engine; +import org.restlet.resource.ClientResource; + +import java.time.Duration; +import java.time.Instant; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ShutdownHookTestCase { + private static final Logger LOGGER = Logger.getLogger("ShutdownHookTest"); + + static { + LOGGER.setLevel(Level.INFO); + } + + /** + * Validates that the server stops immediately when no requests are currently handled. + */ + @Test + public void whenServerIsNotHandlingRequestThenItStopsImmediately() throws Exception { + // Given + final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that + final Restlet hangingRestlet = newHangingRestlet(requestHangingTime); + final Duration shutdownTimeout = Duration.ofSeconds(2); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + // When + final Instant serverAskedToStopInstant = stopServer(server); + + // Then + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests are aborted immediately when graceful shutdown is OFF. + * + * This is done by making a request froze, then stopping the server, and checking that it didn't wait. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItStopsImmediately() throws Exception { + // Given + final Lock lock = new Lock(); + final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that + final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + Server server = startServerWithoutGracefulShutdown(hangingRestlet); + + final TestClient testClient = new TestClient(server); + new Thread(testClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(Duration.ofSeconds(2)); + final Instant serverAskedToStopInstant = stopServer(server); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(Duration.ofSeconds(2)); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieved the request"); + assertTrue(testClient.cr.getStatus().isError(), "The request should have ended in error"); + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, testClient.stoppedAt); + assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests are aborted after the server has waited for the timeout when graceful shutdown is ON. + * + * This is done by making a request froze, then stopping the server, and checking that it waited the expected amount of time before shutting down. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItGracefullyWaitsFor2SecondsBeforeStopping() throws Exception { + // Given + final Lock lock = new Lock(); + final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that + final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + + final Duration shutdownTimeout = Duration.ofSeconds(2); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + final TestClient testClient = new TestClient(server); + new Thread(testClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final Instant serverAskedToStopInstant = stopServer(server); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieved the request"); + assertTrue(testClient.cr.getStatus().isError(), "The request should have ended in error"); + + Thread.sleep(100000); + assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, testClient.stoppedAt); + assertIntervalBetweenDatesEquals(toJettyEffectiveTimeout(shutdownTimeout), serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that incoming requests are refused after the server is stopping when graceful shutdown is ON. + * + * This is done by making a request froze, then stopping the server, and checking that a new request is not taken into account. + */ + @Test + public void whenServerIsHandlingBlockingRequestThenItRefusesNewRequest() throws Exception { + // Given + final Lock lock = new Lock(); + final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that + final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + + final Duration shutdownTimeout = Duration.ofSeconds(2); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + final TestClient firstTestClient = new TestClient(server); + new Thread(firstTestClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final Instant serverAskedToStopInstant = stopServer(server); + final TestClient blockedTestClient = new TestClient(server); + blockedTestClient.run(); + final boolean isFirstClientResourceUnlocked = firstTestClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final boolean isBlockedClientResourceUnlocked = blockedTestClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isFirstClientResourceUnlocked, "The first client didn't achieved the request"); + assertTrue(isBlockedClientResourceUnlocked, "The \"blocked\" client didn't achieved the request"); + assertTrue(firstTestClient.cr.getStatus().isError(), "The request should have ended in error"); + assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, firstTestClient.stoppedAt); + assertTrue(blockedTestClient.cr.getStatus().isConnectorError(), "Any new client is blocked and fails with a connection error"); + assertIntervalBetweenDatesEquals(toJettyEffectiveTimeout(shutdownTimeout), serverAskedToStopInstant, Instant.now()); + } + + /** + * Validates that hanging requests for a short amount of time are handled before the server has waited for the timeout when graceful shutdown is ON. + * + * This is done by making a request froze for a short amount of time, then stopping the server, and checking that the request has been handled. + */ + @Test + public void whenServerIsHandlingLongRequestThenRequestIsHandledCorrectlyBeforeStopping() throws Exception { + // Given + final Lock lock = new Lock(); + final Duration requestHangingTime = Duration.ofSeconds(2); + final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + + final Duration shutdownTimeout = Duration.ofSeconds(20); + final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + + final TestClient testClient = new TestClient(server); + new Thread(testClient).start(); + + // When + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + final Instant serverAskedToStopInstant = stopServer(server); + LOGGER.fine("Client resource wait lock"); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + LOGGER.fine("Client resource unlocked"); + + // Then + assertTrue(isResourceUnlocked, "The resource didn't receive the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieve the request"); + assertEquals(Status.SUCCESS_OK, testClient.cr.getStatus()); + assertIntervalBetweenDatesIsLessThan(requestHangingTime, serverAskedToStopInstant, Instant.now()); + assertEquals("hello, world", testClient.responseText); + } + + private static void assertIntervalBetweenDatesEquals(final Duration expectedDuration, final Instant firstInstant, final Instant secondInstant) { + final Duration dateDifference = Duration.between(firstInstant, secondInstant) + .abs(); + + final Duration tolerance = Duration.ofMillis(200); // let's consider that +/- 200ms is fine + final boolean isDateDifferenceNearlyEqualToExpectedDuration = dateDifference.minus(expectedDuration) + .abs() + .minus(tolerance) + .isNegative(); + assertTrue(isDateDifferenceNearlyEqualToExpectedDuration, String.format("Expected delay: %d second(s) versus %d second(s)\n", expectedDuration.getSeconds(), dateDifference.getSeconds())); + } + + private static void assertIntervalBetweenDatesIsLessThan(final Duration expectedDuration, final Instant firstInstant, final Instant secondInstant) { + final Duration dateDifference = Duration.between(firstInstant, secondInstant).abs(); + + final Duration tolerance = Duration.ofMillis(200); // let's consider that +/- 200ms is fine + final boolean dateDifferenceIsLessThanExpectedDuration = dateDifference.minus(expectedDuration) + .abs() + .minus(tolerance) + .isNegative(); + assertTrue(dateDifferenceIsLessThanExpectedDuration, String.format("difference between dates [%d second(s)] is higher than expected: %d second(s)\n", dateDifference.getSeconds(), expectedDuration.getSeconds())); + } + + private Server startServerWithoutGracefulShutdown(final Restlet restlet) throws Exception { + return startServer(false, Duration.ZERO, restlet); + } + + private Server startServerWithGracefulShutdown(final Duration timeout, final Restlet restlet) throws Exception { + return startServer(true, timeout, restlet); + } + + private Server startServer(final boolean graceful, final Duration timeout, final Restlet restlet) throws Exception { + + Engine.getInstance().getRegisteredServers().add(0, new HttpServerHelper(null)); // Creates a Jetty server helper manually + // 0 port means it will be computed when the server starts + Server server = new Server(new Context(), singletonList(Protocol.HTTP), null, 0, restlet, HttpServerHelper.class.getCanonicalName()); + + if (graceful) { + server.getContext().getParameters().add("lowResource.idleTimeout", Long.toString(timeout.toMillis() * 10)); + } + server.getContext().getParameters().add("shutdown.gracefully", Boolean.toString(graceful)); + server.getContext().getParameters().add("shutdown.timeout", Long.toString(timeout.toMillis())); + + server.start(); + LOGGER.fine( "Server started on port " + server.getEphemeralPort()); + return server; + } + + /** + * Creates a resource that hangs for a specific amount of time before answering and acknowledges incoming requests + * by unlocking the given lock. + */ + private static Restlet newHangingAndLockedRestlet(final Duration requestHangingTime, final Lock lock) { + return new Restlet() { + @Override + public void handle(final Request request, final Response response) { + LOGGER.fine("Restlet opens lock"); + lock.unlock(); + LOGGER.fine("Restlet starts sleeping"); + try { + Thread.sleep(requestHangingTime.toMillis()); + } catch (Exception e) { + // silently stops, especially when Jetty server will abruptly quit after time out + LOGGER.log(Level.FINE, "Restlet error", e); + } + LOGGER.fine("Restlet woke up, answering"); + response.setEntity("hello, world", MediaType.TEXT_ALL); + } + }; + } + + /** + * Creates a resource that hangs for a specific amount of time before answering. + */ + private static Restlet newHangingRestlet(final Duration requestHangingTime) { + return new Restlet() { + @Override + public void handle(final Request request, final Response response) { + try { + Thread.sleep(requestHangingTime.toMillis()); + } catch (Exception e) { + // silently stops, especially when Jetty server will abruptly quit after time out + LOGGER.log(Level.FINE, "Restlet error", e); + } + LOGGER.log(Level.FINE, "Restlet woke up, answering"); + response.setEntity("hello, world", MediaType.TEXT_ALL); + } + }; + } + + private static class TestClient implements Runnable { + private final ClientResource cr; + private Instant stoppedAt = null; + private final Lock lock; // ensures that the client has totally run + private String responseText; + + public TestClient(final Server server) { + cr = new ClientResource("http://localhost:" + server.getEphemeralPort()); + cr.setRetryOnError(false); + this.lock = new Lock(); + } + + @Override + public void run() { + try { + LOGGER.log(Level.FINE, "Client running"); + responseText = cr.get().getText(); + } catch (Exception e) { // silently ignore errors + LOGGER.log(Level.FINE, "Client error", e); + } finally { + stoppedAt = Instant.now(); + lock.unlock(); + LOGGER.log(Level.FINE, "Client state: " + cr.getStatus()); + } + } + } + + private synchronized Instant stopServer(final Server server) { + LOGGER.log(Level.FINE, "Server stopping"); + Instant serverAskedToStopInstant = Instant.now(); + try { + final HttpServerHelper serverHelper = (HttpServerHelper) server.getContext().getAttributes().get("org.restlet.engine.helper"); + serverHelper.getWrappedServer().stop(); + LOGGER.log(Level.FINE, "Server stopped"); + } catch (Exception e) { + // silently ignore errors + LOGGER.log(Level.FINE, "Server stopped", e); + } + return serverAskedToStopInstant; + } + + /** + * Returns the effective Jetty timeout since there is an extra half-timeout in the {@link org.eclipse.jetty.util.thread.QueuedThreadPool}. + */ + private Duration toJettyEffectiveTimeout(final Duration timeout) { + return timeout.multipliedBy(3).dividedBy(2); // FIXME: needs improvements + } + +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java index c263d83328..e465331a96 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java @@ -11,15 +11,16 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.restlet.Application; -import org.restlet.Component; -import org.restlet.Server; +import org.restlet.*; import org.restlet.data.Protocol; import org.restlet.engine.Engine; import org.restlet.engine.adapter.HttpServerHelper; import org.restlet.engine.connector.ClientHelper; +import org.restlet.engine.connector.HttpClientHelper; import org.restlet.engine.connector.ServerHelper; +import java.util.List; +import java.util.logging.Level; import java.util.stream.Stream; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @@ -32,79 +33,65 @@ * @author Jerome Louvel */ public abstract class BaseConnectorsTestCase { - private Component component; private int port; - protected abstract void doTestUri(String uri) throws Exception; + /** + * The method to implement in the tests + * + * @param serverPort The port that the server is listening to. + * @throws Exception + */ + protected abstract void doTest(final int serverPort) throws Exception; - protected Server configureServer(final Component component) { - // server.getContext().getParameters().add("tracing", "true"); - return component.getServers().add(Protocol.HTTP, 0); + protected boolean shouldDebug() { + return false; } - protected abstract Application createApplication(); + protected Server configureServer(final Component component) { + Server server = component.getServers().add(Protocol.HTTP, 0); + server.getContext().getParameters().add("threadPool.minThreads", "1"); + server.getContext().getParameters().add("threadPool.maxThreads", "10"); - protected String getCallUri(final int port) { - return "http://localhost:" + port + "/test"; + if (shouldDebug()) { + server.getContext().getParameters().add("tracing", "true"); + } + + return server; } - protected Stream listTestCases() { - return Stream.of( - // new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), // restore while taking care of #1444 + protected abstract Application createApplication(); + + protected List listTestCases() { + return List.of( + new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.INTERNAL) ); } @TestFactory - Stream dynamicTestsFromStream() { - return listTestCases() + Stream testsFactory() { + return listTestCases().stream() .map(testCase -> dynamicTest( testCase.getTestLabel(), - () -> { - runTest(testCase.httpServer, testCase.httpClient); - resetEngine(); - })); + () -> runTest(testCase.httpServer, testCase.httpClient))); } private void runTest(final HttpServer server, final HttpClient client) throws Exception { - // Engine.setLogLevel(Level.FINE); - Engine nre = Engine.register(false); - nre.getRegisteredServers().add(server.serverHelper); - nre.getRegisteredClients().add(client.clientHelper); - nre.registerDefaultAuthentications(); - nre.registerDefaultConverters(); + if (shouldDebug()) { + System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); + System.setProperty("sun.net.www.protocol.http.HttpURLConnection.LEVEL", "ALL"); + } + initEngine(server, client); start(); try { - doTestUri(getCallUri(port)); + doTest(port); } finally { stop(); - } - } - - public enum HttpServer { - INTERNAL_HTTP(new org.restlet.engine.connector.HttpServerHelper(null)), - INTERNAL_HTTPS(new org.restlet.engine.connector.HttpsServerHelper(null)), - JETTY_HTTP(new org.restlet.ext.jetty.HttpServerHelper(null)), - JETTY_HTTPS(new org.restlet.ext.jetty.HttpsServerHelper(null)); - - final ServerHelper serverHelper; - - HttpServer(HttpServerHelper serverHelper) { - this.serverHelper = serverHelper; - } - } - - public enum HttpClient { - INTERNAL(new org.restlet.engine.connector.HttpClientHelper(null)), JETTY(new org.restlet.ext.jetty.HttpClientHelper(null)); - - final ClientHelper clientHelper; - - HttpClient(ClientHelper clientHelper) { - this.clientHelper = clientHelper; + resetEngine(); } } @@ -125,9 +112,44 @@ private void stop() throws Exception { this.component = null; } + private void initEngine(HttpServer server, HttpClient client) { + if (shouldDebug()) { + Engine.setLogLevel(Level.FINE); + } + + Engine nre = Engine.register(false); + nre.getRegisteredServers().add(server.serverHelper); + nre.getRegisteredClients().add(client.clientHelper); + nre.registerDefaultAuthentications(); + nre.registerDefaultConverters(); + } + private void resetEngine() { // Restore a clean engine org.restlet.engine.Engine.register(); } + public enum HttpServer { + INTERNAL_HTTP(new org.restlet.engine.connector.HttpServerHelper(null)), + INTERNAL_HTTPS(new org.restlet.engine.connector.HttpsServerHelper(null)), + JETTY_HTTP(new org.restlet.ext.jetty.HttpServerHelper(null)), + JETTY_HTTPS(new org.restlet.ext.jetty.HttpsServerHelper(null)); + + final ServerHelper serverHelper; + + HttpServer(HttpServerHelper serverHelper) { + this.serverHelper = serverHelper; + } + } + + public enum HttpClient { + INTERNAL(new HttpClientHelper(null)), JETTY(new org.restlet.ext.jetty.HttpClientHelper(null)); + + final ClientHelper clientHelper; + + HttpClient(ClientHelper clientHelper) { + this.clientHelper = clientHelper; + } + } + } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java index 618d868d49..5d37c49c73 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingPutTestCase.java @@ -19,6 +19,7 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -32,7 +33,9 @@ public class ChunkedEncodingPutTestCase extends BaseConnectorsTestCase { private static final int LOOP_NUMBER = 200; @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + for (int testIndex = 0; testIndex < LOOP_NUMBER; testIndex++) { sendPut(testIndex, uri, 10); } @@ -50,7 +53,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", PutTestResource.class); + router.attachDefault(PutTestResource.class); return router; } }; @@ -95,12 +98,12 @@ private void sendPut(int testIndex, final String uri, final int size) throws Exc System.out.println(response.getStatus()); } - assertNotNull(response.getEntity(), String.format("test #%d - size %d: response's entity is null", testIndex, size)); + assertNotNull(response.getEntity(), format("test #%d - size %d: response's entity is null", testIndex, size)); final String responseEntity = response.getEntity().getText(); - assertNotNull(responseEntity, String.format("test #%d - size %d: response's entity content is null", testIndex, size)); - assertEquals(size, responseEntity.length(), String.format("test #%d - size %d: length of response's entity is wrong", testIndex, size)); + assertNotNull(responseEntity, format("test #%d - size %d: response's entity content is null", testIndex, size)); + assertEquals(size, responseEntity.length(), format("test #%d - size %d: length of response's entity is wrong", testIndex, size)); final String expectedResponseEntity = createChunkedRepresentation(size).getText(); - assertEquals(expectedResponseEntity, responseEntity, String.format("test #%d - size %d: response's entity is wrong", testIndex, size)); + assertEquals(expectedResponseEntity, responseEntity, format("test #%d - size %d: response's entity is wrong", testIndex, size)); } finally { response.release(); client.stop(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java index 28124fe493..78667d6005 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ChunkedEncodingTestCase.java @@ -21,6 +21,7 @@ import java.io.IOException; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.*; /** @@ -37,7 +38,7 @@ private static void assertXML(int testIndex, Representation entity) { try { String expected = ""; String text = entity.getText(); - assertEquals(expected, text, String.format("test #%d: xml representation is wrong", testIndex)); + assertEquals(expected, text, format("test #%d: xml representation is wrong", testIndex)); } catch (IOException ex) { fail(ex.getMessage()); } @@ -59,7 +60,9 @@ private static Representation createTestXml() { } @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + for (int testIndex = 0; testIndex < LOOP_NUMBER; testIndex++) { sendGet(testIndex, uri); sendPut(testIndex, uri); @@ -72,7 +75,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", PutTestResource.class); + router.attachDefault(PutTestResource.class); return router; } }; @@ -84,7 +87,7 @@ private void sendGet(int testIndex, String uri) throws Exception { final Response response = client.handle(request); try { - assertEquals(Status.SUCCESS_OK, response.getStatus(), String.format("test #%d: response's status is wrong", testIndex)); + assertEquals(Status.SUCCESS_OK, response.getStatus(), format("test #%d: response's status is wrong", testIndex)); assertXML(testIndex, response.getEntity()); } finally { response.release(); @@ -99,7 +102,7 @@ private void sendPut(int testIndex, String uri) throws Exception { try { assertChunkedHeader(response); - assertEquals(Status.SUCCESS_OK, response.getStatus(), String.format("test #%d: response's status is wrong", testIndex)); + assertEquals(Status.SUCCESS_OK, response.getStatus(), format("test #%d: response's status is wrong", testIndex)); assertXML(testIndex, response.getEntity()); } finally { response.release(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java index 2a98395d6b..ec85ec8285 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetChunkedTestCase.java @@ -18,6 +18,7 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -29,8 +30,11 @@ public class GetChunkedTestCase extends BaseConnectorsTestCase { private static final String text = "" + "a".repeat(1000) + ""; + @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); @@ -51,7 +55,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetChunkedTestResource.class); + router.attachDefault(GetChunkedTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java index 0f4a1b1be5..113fa028c1 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetQueryParamTestCase.java @@ -22,6 +22,7 @@ import java.util.SortedMap; import java.util.TreeMap; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -32,12 +33,9 @@ public class GetQueryParamTestCase extends BaseConnectorsTestCase { @Override - protected String getCallUri(int port) { - return super.getCallUri(port) + "?q1=a&q2=b"; - } + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d?q1=a&q2=b", serverPort); - @Override - protected void doTestUri(String uri) throws Exception { final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); @@ -56,7 +54,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java index 6538e659f6..b4f6366aaf 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/GetTestCase.java @@ -9,13 +9,7 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; +import org.restlet.*; import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.data.Status; @@ -23,6 +17,9 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Test that a simple get works for all the connectors. * @@ -31,7 +28,9 @@ public class GetTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Request request = new Request(Method.GET, uri); final Client client = new Client(Protocol.HTTP); final Response response = client.handle(request); @@ -54,7 +53,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java index b7b8d5544c..fbc273b266 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/PostPutTestCase.java @@ -9,6 +9,7 @@ package org.restlet.ext.jetty.connectors; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -30,7 +31,9 @@ public class PostPutTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); try { testCall(client, Method.POST, uri); @@ -50,7 +53,7 @@ public Restlet createInboundRoot() { public void handle(Request request, Response response) { Representation entity = request.getEntity(); if (entity != null) { - Form form = new Form(entity); + final Form form = new Form(entity); response.setEntity(form.getWebRepresentation()); } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java index b7bc4fb8e6..305c27b441 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java @@ -9,19 +9,7 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.Enumeration; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; +import org.restlet.*; import org.restlet.data.MediaType; import org.restlet.data.Method; import org.restlet.data.Protocol; @@ -32,6 +20,15 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Test that the client address is available for all the connectors * @@ -40,7 +37,9 @@ public class RemoteClientAddressTestCase extends BaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + final Client client = new Client(Protocol.HTTP); final Request request = new Request(Method.GET, uri); final Response response = client.handle(request); @@ -59,7 +58,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", RemoteClientAddressResource.class); + router.attachDefault(RemoteClientAddressResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java new file mode 100644 index 0000000000..c575f3c06f --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java @@ -0,0 +1,143 @@ +/** + * Copyright 2005-2024 Qlik + *

+ * The contents of this file is subject to the terms of the Apache 2.0 open + * source license available at http://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ + +package org.restlet.ext.jetty.connectors; + +import org.restlet.*; +import org.restlet.data.Method; +import org.restlet.data.Parameter; +import org.restlet.data.Protocol; +import org.restlet.data.Status; +import org.restlet.engine.connector.HttpClientHelper; +import org.restlet.util.Series; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * This tests the ability of the server to accept a fixed number of incoming connections. + */ +public class ServerMaxConnectionsTestCase extends BaseConnectorsTestCase { + private static final Logger LOGGER = Logger.getLogger(ServerMaxConnectionsTestCase.class.getCanonicalName()); + private final static int CONNECTIONS_NUMBER = 1; + private final static int CONCURRENT_REQUESTS = 2; + private final static Duration SERVER_RESOURCE_FREEZE_DURATION = Duration.ofSeconds(1); + + @Override + protected Server configureServer(Component component) { + final Server server = super.configureServer(component); + + final Series parameters = server.getContext().getParameters(); + parameters.add("server.maxConnections", Integer.toString(CONNECTIONS_NUMBER)); + parameters.add("connector.acceptors", Integer.toString(CONCURRENT_REQUESTS)); // server can accept all requests + + return server; + } + + @Override + protected void doTest(final int serverPort) throws Exception { + final String uri = format("http://localhost:%d", serverPort); + + final CountDownLatch countDownLatch = new CountDownLatch(CONCURRENT_REQUESTS); + + final List testResults = new ArrayList<>(); + + final Executor executor = Executors.newFixedThreadPool(CONCURRENT_REQUESTS); + final Runnable runnable = () -> { + testResults.add(sendGet(uri).isSuccess()); + countDownLatch.countDown(); + log("client countDownLatch.countDown done " + Thread.currentThread().getName()); + }; + + for (int i = 0; i < CONCURRENT_REQUESTS; i++) { + executor.execute(runnable); + } + + log("test before countDownLatch.await()"); + countDownLatch.await(); + log("test after countDownLatch.await()"); + + assertEquals(CONCURRENT_REQUESTS, testResults.size()); + assertTrue(testResults.contains(true)); // One has succeeded + assertTrue(testResults.contains(false)); // The other has failed + } + + private static void log(final String message) { + LOGGER.fine(message + " " + Thread.currentThread()); + } + + @Override + protected List listTestCases() { + return List.of( + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL) + ); + } + + private Status sendGet(final String uri) { + final Status result; + + log("client send get " + Thread.currentThread().getName()); + try { + final Request request = new Request(Method.GET, uri + "/" + Thread.currentThread().getName()); + final Client client = new Client(new Context(), Protocol.HTTP); + + if (client.getContext().getAttributes().get("org.restlet.engine.helper") instanceof HttpClientHelper) { + // Specific to the internal client + // When Jetty refuses the extra connection, this does not block the underlying native HttpClient... + // Let's set a timeout higher than the wait time imposed by the server (otherwise all requests will fail) + String readTimeoutInMs = Long.toString(SERVER_RESOURCE_FREEZE_DURATION.plus(Duration.ofSeconds(1)).toMillis()); + client.getContext().getParameters().add("readTimeout", readTimeoutInMs); + } + + final Response response = client.handle(request); + log("client get sent " + Thread.currentThread().getName()); + result = response.getStatus(); + log("client status " + result + " " + Thread.currentThread().getName()); + response.release(); + client.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + log("client done " + Thread.currentThread().getName()); + return result; + } + + @Override + protected Application createApplication() { + return new Application() { + @Override + public Restlet createInboundRoot() { + return new Restlet() { + @Override + public void handle(Request request, Response response) { + try { + log("server resource wait"); + Thread.sleep(SERVER_RESOURCE_FREEZE_DURATION.toMillis()); + log("server resource wait ended"); + response.setStatus(Status.SUCCESS_NO_CONTENT); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } + }; + } + +} diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java index bc23de2422..464923d6b2 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java @@ -10,11 +10,8 @@ package org.restlet.ext.jetty.connectors; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.restlet.*; -import org.restlet.data.Method; import org.restlet.data.Parameter; import org.restlet.data.Protocol; import org.restlet.engine.io.IoUtils; @@ -22,7 +19,7 @@ import java.io.*; import java.nio.file.Files; -import java.util.stream.Stream; +import java.util.List; /** * Base test case that will call an abstract method for several client/server @@ -35,35 +32,17 @@ @SuppressWarnings("unused") public abstract class SslBaseConnectorsTestCase extends BaseConnectorsTestCase { + protected static final String KEYSTORE_FILE_NAME = "dummy.p12"; + protected static final String KEYSTORE_PASSWORD = "testtest"; + protected static final String KEYSTORE_TYPE = "PKCS12"; protected static File testKeystoreFile; - protected void configureSslClientParameters(Context context) { - Series parameters = context.getParameters(); - parameters.add("truststorePath", testKeystoreFile.getPath()); - parameters.add("truststorePassword", "testtest"); - } - - protected void configureSslServerParameters(Context context) { - Series parameters = context.getParameters(); - parameters.add("keystorePath", testKeystoreFile.getPath()); - parameters.add("keystorePassword", "testtest"); - parameters.add("keyPassword", "testtest"); - parameters.add("truststorePath", testKeystoreFile.getPath()); - parameters.add("truststorePassword", "testtest"); - // parameters.add("tracing", "true"); - } - - @Override - protected String getCallUri(final int port) { - return "https://localhost:" + port + "/test"; - } - @BeforeAll public static void globalSetUp() throws IOException { - testKeystoreFile = Files.createTempFile("sslBaseConnectorsTest", "dummy.jks").toFile(); + testKeystoreFile = Files.createTempFile("sslBaseConnectorsTest", KEYSTORE_FILE_NAME).toFile(); testKeystoreFile.delete(); - InputStream resourceAsStream = SslBaseConnectorsTestCase.class.getResourceAsStream("dummy.jks"); + InputStream resourceAsStream = SslBaseConnectorsTestCase.class.getResourceAsStream(KEYSTORE_FILE_NAME); if (resourceAsStream != null) { OutputStream outputStream = new FileOutputStream(testKeystoreFile); IoUtils.copy(resourceAsStream, outputStream); @@ -76,11 +55,11 @@ public static void globalSetUp() throws IOException { } @Override - protected Stream listTestCases() { - return Stream.of( + protected List listTestCases() { + return List.of( new ConnectorTestCase(HttpServer.INTERNAL_HTTPS, HttpClient.JETTY), - // new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.INTERNAL), // restore while taking care of #1444 - // new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.JETTY), // restore while taking care of #1444 + new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.JETTY_HTTPS, HttpClient.JETTY), new ConnectorTestCase(HttpServer.INTERNAL_HTTPS, HttpClient.INTERNAL) ); } @@ -88,11 +67,39 @@ protected Stream listTestCases() { @Override protected Server configureServer(final Component component) { final Server server = component.getServers().add(Protocol.HTTPS, 0); - configureSslServerParameters(server.getContext()); - // server.getContext().getParameters().add("tracing", "true"); + configureSslServerParameters(server); return server; } + protected void configureSslClientParameters(final Client client) { + Series parameters = client.getContext().getParameters(); + + parameters.add("truststorePath", testKeystoreFile.getPath()); + parameters.add("truststorePassword", KEYSTORE_PASSWORD); + parameters.add("trustStoreType", KEYSTORE_TYPE); + if (shouldDebug()) { + parameters.add("tracing", "true"); + } + } + + protected void configureSslServerParameters(final Server server) { + Series parameters = server.getContext().getParameters(); + + parameters.add("keystorePath", testKeystoreFile.getPath()); + parameters.add("keystorePassword", KEYSTORE_PASSWORD); + parameters.add("keyStoreType", KEYSTORE_TYPE); + parameters.add("keyPassword", KEYSTORE_PASSWORD); + + parameters.add("truststorePath", testKeystoreFile.getPath()); + parameters.add("truststorePassword", KEYSTORE_PASSWORD); + parameters.add("trustStoreType", KEYSTORE_TYPE); + + if (shouldDebug()) { + System.setProperty("javax.net.debug", "ssl:handshake"); + parameters.add("tracing", "true"); + } + } + @AfterAll protected static void tearDown() { testKeystoreFile.delete(); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java index 244f5db8ff..1b879632bf 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslClientContextGetTestCase.java @@ -9,23 +9,17 @@ package org.restlet.ext.jetty.connectors; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -import org.restlet.data.MediaType; -import org.restlet.data.Method; -import org.restlet.data.Protocol; -import org.restlet.data.Status; +import org.restlet.*; +import org.restlet.data.*; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.representation.Variant; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import org.restlet.util.Series; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test that a simple get using SSL works for all the connectors. @@ -36,20 +30,33 @@ public class SslClientContextGetTestCase extends SslBaseConnectorsTestCase { @Override - protected void doTestUri(String uri) throws Exception { - final Request request = new Request(Method.GET, uri); - final Client client = new Client(Protocol.HTTPS); - if (client.getContext() == null) { - client.setContext(new Context()); - } - configureSslServerParameters(client.getContext()); - final Response response = client.handle(request); + protected void doTest(final int serverPort) throws Exception { + final String uri = format("https://localhost:%d", serverPort); + + final Response response = sendGet(uri); assertEquals(Status.SUCCESS_OK, response.getStatus(), response.getStatus().getDescription()); assertEquals("Hello world", response.getEntity().getText()); + } + + private Response sendGet(final String uri) { + final Client client = new Client(Protocol.HTTPS); + client.setContext(new Context()); + configureSslClientParameters(client); + + final Request request = new Request(Method.GET, uri); + return client.handle(request); + } + + @Override + protected void configureSslClientParameters(final Client client) { + super.configureSslClientParameters(client); - Thread.sleep(200); - client.stop(); + Series parameters = client.getContext().getParameters(); + parameters.add("keystorePath", testKeystoreFile.getPath()); + parameters.add("keystorePassword", KEYSTORE_PASSWORD); + parameters.add("keyPassword", KEYSTORE_PASSWORD); + parameters.add("keyStoreType", KEYSTORE_TYPE); } @Override @@ -58,7 +65,7 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java index e9140a9037..eabfb38a67 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java @@ -9,22 +9,16 @@ package org.restlet.ext.jetty.connectors; -import org.restlet.Application; -import org.restlet.Client; -import org.restlet.Context; -import org.restlet.Request; -import org.restlet.Response; -import org.restlet.Restlet; -import org.restlet.data.MediaType; -import org.restlet.data.Method; -import org.restlet.data.Protocol; -import org.restlet.data.Status; +import org.restlet.*; +import org.restlet.data.*; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.representation.Variant; import org.restlet.resource.ServerResource; import org.restlet.routing.Router; +import org.restlet.util.Series; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -35,32 +29,26 @@ */ public class SslGetTestCase extends SslBaseConnectorsTestCase { - public static class GetTestResource extends ServerResource { - - public GetTestResource() { + @Override + protected boolean shouldDebug() { + return true; + } - getVariants().add(new Variant(MediaType.TEXT_PLAIN)); - } + @Override + protected void doTest(final int serverPort) throws Exception { + final Response response = sendGet(format("https://localhost:%d", serverPort)); - @Override - public Representation get(Variant variant) { - return new StringRepresentation("Hello world", MediaType.TEXT_PLAIN); - } + assertEquals(Status.SUCCESS_OK, response.getStatus(), response.getStatus().getDescription()); + assertEquals("Hello world", response.getEntity().getText()); } - @Override - protected void doTestUri(String uri) throws Exception { - final Request request = new Request(Method.GET, uri); + private Response sendGet(final String uri) { final Client client = new Client(Protocol.HTTPS); client.setContext(new Context()); - configureSslClientParameters(client.getContext()); - final Response r = client.handle(request); - - assertEquals(Status.SUCCESS_OK, r.getStatus(), r.getStatus().getDescription()); - assertEquals("Hello world", r.getEntity().getText()); + configureSslClientParameters(client); - Thread.sleep(200); - client.stop(); + final Request request = new Request(Method.GET, uri); + return client.handle(request); } @Override @@ -69,9 +57,22 @@ protected Application createApplication() { @Override public Restlet createInboundRoot() { final Router router = new Router(getContext()); - router.attach("/test", GetTestResource.class); + router.attachDefault(GetTestResource.class); return router; } }; } + + public static class GetTestResource extends ServerResource { + + public GetTestResource() { + getVariants().add(new Variant(MediaType.TEXT_PLAIN)); + } + + @Override + public Representation get(Variant variant) { + return new StringRepresentation("Hello world", MediaType.TEXT_PLAIN); + } + } + } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 b/org.restlet.java/org.restlet.ext.jetty/src/test/resources/org/restlet/ext/jetty/connectors/dummy.p12 new file mode 100644 index 0000000000000000000000000000000000000000..94519e717d66bec98a3aa5a44bb3f04bf89e0373 GIT binary patch literal 2726 zcma);XE+;-7RQrFY^AM|RP7QJjgcC!O^6t^m8v~T?M*3SM~xyz&8jU{(W|2 zMp3h+W=M^<_qq4!``l0WemKv0&hLNz=i~o52rP902uOy&Qfrb^2}Gfzh;%>-U_O>w z7L28q`i+qYEQIi15+nzVg=GH5_}}%6oaR3+T52FD9}7|V4P_Aae;g1xgb70OpOF@E z3(U9|xTdxaFB2W7pvEmN+^aemY@q>x;0z!j2ZENI^4}LhR1g4ym7K~l3JtU)0|N!X z4A*zPUTZzAd;w4&)->2FO$KAZquJq!M}dg&WWWp^d)N=)cRxNxZ^F6%rc{%A6BkI< z{xA{6QfVcAA5cZG_14yRQj;;7i{5@W=iJl_NT|EYqWgAmM>VF61%a{BNaXwrlgy3} zm^~bf6oe0v%CQ%W$U7-TBKhEJa547fBI%kq;oZ?Gu1qNky~DPK%XqiG8ZX#FX4ePa z)N$(!KQLFcOXKSYU3KIpx_-W1w8mzo8!Tle4d!ekV)?3DIu4E51%}Y}5MH0qYK_$5 z8j)7k9b$3gCJY(jWc7V?awtuF$T9C;OA>V1tAssm+>ieaaa1KjyU9CI=jsNa^&_ zpWu&OFw+?_n7N%?ZPtGru;HszZ>d7VW|NjXI3NGoGCb&>S^cY|F5)WOb(oEv*2;>iXmC2lEH_Wk!D|EPzYz&ch|COX!QK~_C?OS%Pd^q0x328cUmHgzLd4-oCG;hD9E-&A=$rEJc|X^;cK(FSOTS@QaUOai5N>9;R3< zdr;A(>aF(8QE9fDF)1dRw4d%>V#dGhP?HuYudTSe`<3KiKldvz{UW*8`7Ny;kV?#8 zwG2574_{xj>&hX!n!@iKQ`a{D`U&5cs^oI*NPw`pqk&OxTvMcwL3}Wa#*)!99O!b>vVqlDWK6X! z;;cFY`Z(s%p$Bou6V5bHzDu^Dbt{L3@-zcvnHM%8=!8bJ7KVpVEpM9-M@W*!c^{2s)=6y{t*?t;ZfKjtr6v(SSOWDB*C)tMvnn9%aNgc6eGH()4V_C1ciM` z5+qAEoZMWwx20rY+&MHyv7R>GS;^+|e5kWh&{XOXKNHs1(}ZbI+W z*U`G|U|{<(jTfCwRZ`!YiQGdRxHjN>1oUI9Tcj<0i5ByUd6mPaVXT7TH*)qAT%ZK( z;V2zZDt_O}YPIb&wnajcGqwKW*R+YyesggLWX@bk+8M?_qZqSR5cQ%n9+PtCVu|&5 z>lx35h#Ug;CoZWNMQGWm0WN?5z$1ViAOOMnk919h5q!nK(Z!BKL_!=PCN3c&DK05` z2Z05L{aK{G$N>S4i6z5c=ElieA6x3E|rQWr}w_{I>f4gmjta z=XIjKC~m9Seh5XTmlld8`x&3zy&i>g3#=%ezkt@5mVf6XF2k@M(@EXp_c_r)^O|Cn zXqS>|_Pd{+&6Y0%0&=%X8dtBT3el4+lxyADd+_*Zg7tyRndqhBb$>k!f;*R2st+Dq zeXBy|L`8W|aO3uTfhT*FtozQ&Fjv`Ft`OtX8y*7bC#C?K3Q&iwiXwk;ROxn!7R!2p zb3?VA)x!!uq8DtGq7$=zRFwo{`#{=^f z=1l1g&60NAoAx4m9x#jevH&xErae6tSAZEWapCmN`@% z6bAf(4KMW9UQ!p1!%=K;o(RgdFwF1E_26)MBfe8i0k3_Ekaxx|Jddtyy;%|2X2YBX zUY`Y*BfU5#CzjuZrl_a=bchR6AKRu^epMzqU3i__CYv9d2PP@y+Dd(yS7Q57pgcj& zl)!5nG^hkdF)rv{Gfh`=1mCjlNMcwyny70!RC-1|C+vbxC!(uJ3 zljs!IYPnN5T_GrbE^hS~M1DDIlt8!4Y4@cGzKZ_bm*=@J|=^j;lGDoEVyAU4< zr#tJT%wWa2yiXIJW3MVqBX5Fon~|>cy+oe*C33N~xm6lX7E-L``c8eQ>_C_6a#50_ z{!JI#p~~|5=aGplBY8CO*P2PdAR8WA$10}Ms9kTXdy(6vSuTDGhhmHtJ z1V6{W73BjVgEHP?HCV0)({TC3>K}0*EcVj!ybAT&t(g(r&HHM#eVUEfm~wDBtgIEd z0*`#G_#~mcF>VB=0*$VpdG>tQuYKVOuJrsnuxEU}3AxnwR+fwBg!F`R&AM=6x1IXK z_);w;wz1IbI7cD1GbnGWc8EXpFipDuiY|r>nt5AAG8RR6FV$wIRosk$Oh1t>&~rNA zCT3Z{ILCP*M`Lq5!^td35T#|_+ptt< diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java index 9a1e3d8c84..7d450cb4cb 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpClientHelper.java @@ -89,9 +89,9 @@ * the {@link #getHostnameVerifier()} method for details. *

* Note that by default, the {@link HttpURLConnection} class as implemented by - * Sun will retry a request if an IO exception is caught, for example due to a + * Sun will retry a request if an IO exception is caught, for example, due to a * connection reset by the server. This can be annoying, especially because the - * HTTP semantics of non idempotent methods like POST can be broken, but also + * HTTP semantics of non-idempotent methods like POST can be broken, but also * because the new request won't include an entity. There is one way to disable * this behavior for POST requests only by setting the system property * "sun.net.http.retryPost" to "false". @@ -159,7 +159,7 @@ public HostnameVerifier getHostnameVerifier() { /** * Returns the read timeout value. A timeout of zero is interpreted as an - * infinite timeout. Defaults to 60000. + * infinite timeout. Default to 60000. * * @return The read timeout value. */ @@ -178,9 +178,9 @@ public boolean isAllowUserInteraction() { } /** - * Indicates if the protocol will automatically follow redirects. + * Indicates if the protocol automatically follows redirects. * - * @return True if the protocol will automatically follow redirects. + * @return True if the protocol automatically follows redirects. */ public boolean isFollowRedirects() { return Boolean.parseBoolean(getHelpedParameters().getFirstValue("followRedirects", "false")); diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java index 5d35ad8a3b..475414663a 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/connector/HttpUrlConnectionCall.java @@ -48,7 +48,7 @@ public class HttpUrlConnectionCall extends ClientCall { * @param helper The parent HTTP client helper. * @param method The method name. * @param requestUri The request URI. - * @param hasEntity Indicates if the call will have an entity to send to the + * @param hasEntity Indicates if the call has an entity to send to the * server. * @throws IOException */ @@ -60,8 +60,7 @@ public HttpUrlConnectionCall(HttpClientHelper helper, String method, String requ URL url = new URL(requestUri); this.connection = (HttpURLConnection) url.openConnection(); - // These properties can only be used with Java 1.5 and upper - // releases + // These properties can only be used with Java 1.5 and upper releases int majorVersionNumber = SystemUtils.getJavaMajorVersion(); int minorVersionNumber = SystemUtils.getJavaMinorVersion(); if ((majorVersionNumber > 1) || ((majorVersionNumber == 1) && (minorVersionNumber >= 5))) { @@ -201,7 +200,7 @@ public Series

getResponseHeaders() { headerName = getConnection().getHeaderFieldKey(i); headerValue = getConnection().getHeaderField(i); } catch (java.util.NoSuchElementException e) { - // Some implementations especially the one for Google App + // Some implementations, especially the one for Google App // Engine throws a NoSuchElementException though this is not // stated by the contract of the abstract class // HttpUrlConnection. @@ -212,7 +211,7 @@ public Series
getResponseHeaders() { } else { // As stated by the HttpUrlConnection javadocs, some // implementations may treat the 0th header field as - // special, i.e. as the status line returned by the HTTP + // special, i.e., as the status line returned by the HTTP // server. loop = (i == 0); } @@ -249,8 +248,8 @@ public int getStatusCode() throws IOException { } /** - * Sends the request to the client. Commits the request line, headers and - * optional entity and send them over the network. + * Sends the request to the client. + * Commits the request line, headers, and optional entity and send them over the network. * * @param request The high-level request. * @return The result status. diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java index d56b2fa9d4..0b80dce2cd 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java @@ -268,10 +268,13 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { if ((this.keyStorePath != null) || (this.keyStoreProvider != null) || (this.keyStoreType != null)) { // Loads the key store. - KeyStore keyStore = (this.keyStoreProvider != null) - ? KeyStore.getInstance((this.keyStoreType != null) ? this.keyStoreType : KeyStore.getDefaultType(), - this.keyStoreProvider) - : KeyStore.getInstance((this.keyStoreType != null) ? this.keyStoreType : KeyStore.getDefaultType()); + final String nonNullKeyStoreType = (this.keyStoreType != null) + ? this.keyStoreType + : KeyStore.getDefaultType(); + final KeyStore keyStore = (this.keyStoreProvider != null) + ? KeyStore.getInstance(nonNullKeyStoreType, this.keyStoreProvider) + : KeyStore.getInstance(nonNullKeyStoreType); + FileInputStream keyStoreInputStream = null; try { @@ -294,12 +297,11 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { if ((this.trustStorePath != null) || (this.trustStoreProvider != null) || (this.trustStoreType != null)) { // Loads the trust store. + String nonNullTrustStoreType = (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType(); KeyStore trustStore = (this.trustStoreProvider != null) - ? KeyStore.getInstance( - (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType(), - this.trustStoreProvider) - : KeyStore.getInstance( - (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType()); + ? KeyStore.getInstance(nonNullTrustStoreType, this.trustStoreProvider) + : KeyStore.getInstance(nonNullTrustStoreType); + FileInputStream trustStoreInputStream = null; try { @@ -329,7 +331,7 @@ public javax.net.ssl.SSLContext createSslContext() throws Exception { sslContext.init(kmf != null ? kmf.getKeyManagers() : null, tmf != null ? tmf.getTrustManagers() : null, sr); // Wraps the SSL context to be able to set cipher suites and other - // properties after SSL engine creation for example + // properties after SSL engine creation, for example result = createWrapper(sslContext); return result; } diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java index 46233894c8..c799514448 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/SslUtils.java @@ -92,7 +92,7 @@ public static Integer extractKeySize(String sslCipherSuite) { } /** - * Returns the SSL context factory. It first look for a "sslContextFactory" + * Returns the SSL context factory. It first looks for a "sslContextFactory" * attribute (instance), then for a "sslContextFactory" parameter (class name to * instantiate). * @@ -102,7 +102,8 @@ public static Integer extractKeySize(String sslCipherSuite) { */ public static SslContextFactory getSslContextFactory(RestletHelper helper) { - SslContextFactory result = (SslContextFactory) ((helper.getContext() == null) ? null + SslContextFactory result = (SslContextFactory) ((helper.getContext() == null) + ? null : helper.getContext().getAttributes().get("sslContextFactory")); if (result == null) { diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java index 1c99c8c0e1..df5339143f 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/security/CertificateAuthenticator.java @@ -44,7 +44,7 @@ public CertificateAuthenticator(Context context) { /** * Extracts the Principal of the subject to use from a chain of certificate. By - * default, this is the X500Principal of the subject subject of the first + * default, this is the X500Principal of the subject of the first * certificate in the chain. * * @see X509Certificate @@ -55,7 +55,7 @@ public CertificateAuthenticator(Context context) { protected List getPrincipals(List certificateChain) { ArrayList principals = null; - if ((certificateChain != null) && (certificateChain.size() > 0)) { + if ((certificateChain != null) && (!certificateChain.isEmpty())) { Certificate userCert = certificateChain.get(0); if (userCert instanceof X509Certificate) { diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java index 3f337826b5..62b7635b59 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java @@ -205,7 +205,7 @@ public T getFirst(String name, boolean ignoreCase) { /** * Returns the value of the first parameter found with the given name. * - * @param name The parameter name (case sensitive). + * @param name The parameter name (case-sensitive). * @return The value of the first parameter found with the given name. */ public String getFirstValue(String name) { @@ -216,7 +216,7 @@ public String getFirstValue(String name) { * Returns the value of the first parameter found with the given name. * * @param name The parameter name. - * @param ignoreCase Indicates if the name comparison is case sensitive. + * @param ignoreCase Indicates if the name comparison is case-sensitive. * @return The value of the first parameter found with the given name. */ public String getFirstValue(String name, boolean ignoreCase) { diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java index 4222d30e2c..6e6d755e71 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java @@ -39,7 +39,7 @@ private void testCall(Context context, Method method, String uri) /** * Tests the cookies parsing. */ - @Test + // @Test TODO why does it fail in CI? public void testRedirect() throws Exception { // Create components final Component clientComponent = new Component(); @@ -61,14 +61,11 @@ public void testRedirect() throws Exception { @Override public void handle(Request request, Response response) { // Print the requested URI path - final String message = "Resource URI: " - + request.getResourceRef() + '\n' + "Base URI: " - + request.getResourceRef().getBaseRef() + '\n' - + "Remaining part: " - + request.getResourceRef().getRemainingPart() + '\n' + final String message = "Resource URI: " + request.getResourceRef() + '\n' + + "Base URI: " + request.getResourceRef().getBaseRef() + '\n' + + "Remaining part: " + request.getResourceRef().getRemainingPart() + '\n' + "Method name: " + request.getMethod() + '\n'; - response.setEntity(new StringRepresentation(message, - MediaType.TEXT_PLAIN)); + response.setEntity(new StringRepresentation(message, MediaType.TEXT_PLAIN)); } }; From f0c1b8cb4897516274266676844f09c1d6dc28f1 Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Mon, 24 Feb 2025 20:34:38 +0100 Subject: [PATCH 2/6] Configure server correctly with graceful shutdown --- .../main/java/org/restlet/ext/jetty/JettyServerHelper.java | 6 +++++- .../ext/jetty/connectors/BaseConnectorsTestCase.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java index 31fbf2be82..03763a4cfa 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java @@ -338,7 +338,11 @@ private org.eclipse.jetty.server.Server createServer() { } jettyServer.setStopAtShutdown(getShutdownGracefully()); - jettyServer.setStopTimeout(getShutdownTimeout()); + if (getShutdownGracefully()) { + jettyServer.setStopTimeout(getShutdownTimeout()); + } else { + jettyServer.setStopTimeout(0); + } jettyServer.setHandler(createJettyHandler()); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java index e465331a96..871db991a0 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java @@ -52,6 +52,7 @@ protected Server configureServer(final Component component) { Server server = component.getServers().add(Protocol.HTTP, 0); server.getContext().getParameters().add("threadPool.minThreads", "1"); server.getContext().getParameters().add("threadPool.maxThreads", "10"); + server.getContext().getParameters().add("shutdown.gracefully", "false"); if (shouldDebug()) { server.getContext().getParameters().add("tracing", "true"); From 61c52026a6a22225d3f6599db81fd034e29abe45 Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Mon, 24 Feb 2025 21:32:29 +0100 Subject: [PATCH 3/6] Run tests with internal client first, they take less time to load from scratch than the ones based on Jetty client --- .../restlet/ext/jetty/HttpClientHelper.java | 83 ++++++++++--------- .../connectors/BaseConnectorsTestCase.java | 6 +- .../RemoteClientAddressTestCase.java | 17 ++-- 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java index 4a552103ce..6e534e77d5 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java @@ -281,48 +281,25 @@ protected HttpClient createHttpClient() { } HttpClientTransport httpTransport = null; - HTTP2Client http2Client = null; - HTTP3Client http3Client = null; - switch (getHttpClientTransportMode()) { + final String httpClientTransportMode = getHttpClientTransportMode(); + switch (httpClientTransportMode) { case "HTTP2": - http2Client = new HTTP2Client(); - HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2( - http2Client); - http2Transport.setUseALPN(true); - httpTransport = http2Transport; + httpTransport = getHttpClientTransportForHttp2(); break; - case "HTTP3": - ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(clientQuicConfig); - http3Client.getQuicConfiguration() - .setSessionRecvWindow(64 * 1024 * 1024); - httpTransport = new HttpClientTransportOverHTTP3(http3Client); + httpTransport = getHttpClientTransportForHttp3(sslContextFactory); break; - case "DYNAMIC": - ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; - - http2Client = new HTTP2Client(); - ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( - http2Client); - - ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration( - sslContextFactory, null); - http3Client = new HTTP3Client(quicConfiguration); - ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3( - http3Client); - - HttpClientTransportDynamic httpDynamicTransport = new HttpClientTransportDynamic( - new ClientConnector(), http1, http2, http3); - httpTransport = httpDynamicTransport; + httpTransport = getHttpClientTransportForDynamicMode(sslContextFactory); break; - case "HTTP11": + httpTransport = getHttpTransportForHttp1_1(); + break; default: - httpTransport = new HttpClientTransportOverHTTP(); + getLogger().log(Level.WARNING, + "Unknown HTTP client transport mode: {0}, use HTTP11 instead", httpClientTransportMode); + httpTransport = getHttpTransportForHttp1_1(); break; } @@ -356,11 +333,9 @@ protected HttpClient createHttpClient() { httpClient.setHttpCookieStore(getCookieStore()); httpClient.setIdleTimeout(getIdleTimeout()); - httpClient.setMaxConnectionsPerDestination( - getMaxConnectionsPerDestination()); + httpClient.setMaxConnectionsPerDestination(getMaxConnectionsPerDestination()); httpClient.setMaxRedirects(getMaxRedirects()); - httpClient.setMaxRequestsQueuedPerDestination( - getMaxRequestsQueuedPerDestination()); + httpClient.setMaxRequestsQueuedPerDestination(getMaxRequestsQueuedPerDestination()); httpClient.setMaxResponseHeadersSize(getMaxResponseHeadersSize()); String httpProxyHost = getProxyHost(); @@ -384,6 +359,40 @@ protected HttpClient createHttpClient() { return httpClient; } + private static HttpClientTransportOverHTTP getHttpTransportForHttp1_1() { + return new HttpClientTransportOverHTTP(); + } + + private static HttpClientTransport getHttpClientTransportForHttp2() { + HTTP2Client http2Client = new HTTP2Client(); + HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2(http2Client); + http2Transport.setUseALPN(true); + + return http2Transport; + } + + private static HttpClientTransport getHttpClientTransportForHttp3(SslContextFactory.Client sslContextFactory) { + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration); + http3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); + + return new HttpClientTransportOverHTTP3(http3Client); + } + + private static HttpClientTransport getHttpClientTransportForDynamicMode(SslContextFactory.Client sslContextFactory) { + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + return new HttpClientTransportDynamic(new ClientConnector(), http1, http2, http3); + } + /** * The timeout in milliseconds for the DNS resolution of host addresses. * Defaults to 15000. diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java index 871db991a0..75eb55dcc5 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java @@ -65,10 +65,10 @@ protected Server configureServer(final Component component) { protected List listTestCases() { return List.of( - new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.INTERNAL), - new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY), - new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.INTERNAL) + new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.INTERNAL), + new ConnectorTestCase(HttpServer.INTERNAL_HTTP, HttpClient.JETTY), + new ConnectorTestCase(HttpServer.JETTY_HTTP, HttpClient.JETTY) ); } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java index 305c27b441..dbb72066ed 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/RemoteClientAddressTestCase.java @@ -46,6 +46,7 @@ protected void doTest(final int serverPort) throws Exception { try { assertEquals(Status.SUCCESS_OK, response.getStatus()); + assertEquals("OK", response.getEntityAsText()); } finally { response.release(); client.stop(); @@ -73,15 +74,15 @@ public RemoteClientAddressResource() { @Override public Representation get(Variant variant) { boolean localAddress = false; + try { - Enumeration n = NetworkInterface - .getNetworkInterfaces(); - for (; n.hasMoreElements();) { - NetworkInterface e = n.nextElement(); - Enumeration a = e.getInetAddresses(); - for (; a.hasMoreElements();) { - InetAddress addr = a.nextElement(); - if (addr.getHostAddress().equals( + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + final InetAddress inetAddress = inetAddresses.nextElement(); + if (inetAddress.getHostAddress().equals( getRequest().getClientInfo().getAddress())) { localAddress = true; } From 9a3bca5dc3907291df3580b780f655fecbf5388d Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Mon, 24 Feb 2025 22:54:41 +0100 Subject: [PATCH 4/6] Configure server correctly with graceful shutdown --- .../ext/jetty/connectors/BaseConnectorsTestCase.java | 12 +++++++----- .../connectors/ServerMaxConnectionsTestCase.java | 6 ++---- .../jetty/connectors/SslBaseConnectorsTestCase.java | 10 +++++++--- .../restlet/ext/jetty/connectors/SslGetTestCase.java | 5 ----- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java index 75eb55dcc5..0eed13fe13 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/BaseConnectorsTestCase.java @@ -48,8 +48,11 @@ protected boolean shouldDebug() { return false; } - protected Server configureServer(final Component component) { - Server server = component.getServers().add(Protocol.HTTP, 0); + protected Server createServer(final Component component) { + return component.getServers().add(Protocol.HTTP, 0); + } + + protected void configureServer(final Server server) { server.getContext().getParameters().add("threadPool.minThreads", "1"); server.getContext().getParameters().add("threadPool.maxThreads", "10"); server.getContext().getParameters().add("shutdown.gracefully", "false"); @@ -57,8 +60,6 @@ protected Server configureServer(final Component component) { if (shouldDebug()) { server.getContext().getParameters().add("tracing", "true"); } - - return server; } protected abstract Application createApplication(); @@ -98,7 +99,8 @@ private void runTest(final HttpServer server, final HttpClient client) throws Ex private void start() throws Exception { this.component = new Component(); - Server server = configureServer(this.component); + final Server server = createServer(this.component); + configureServer(server); Application application = createApplication(); this.component.getDefaultHost().attach(application); diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java index c575f3c06f..5319fb13ab 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/ServerMaxConnectionsTestCase.java @@ -39,14 +39,12 @@ public class ServerMaxConnectionsTestCase extends BaseConnectorsTestCase { private final static Duration SERVER_RESOURCE_FREEZE_DURATION = Duration.ofSeconds(1); @Override - protected Server configureServer(Component component) { - final Server server = super.configureServer(component); + protected void configureServer(final Server server) { + super.configureServer(server); final Series parameters = server.getContext().getParameters(); parameters.add("server.maxConnections", Integer.toString(CONNECTIONS_NUMBER)); parameters.add("connector.acceptors", Integer.toString(CONCURRENT_REQUESTS)); // server can accept all requests - - return server; } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java index 464923d6b2..d06037f6f3 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslBaseConnectorsTestCase.java @@ -65,10 +65,14 @@ protected List listTestCases() { } @Override - protected Server configureServer(final Component component) { - final Server server = component.getServers().add(Protocol.HTTPS, 0); + protected Server createServer(Component component) { + return component.getServers().add(Protocol.HTTPS, 0); + } + + @Override + protected void configureServer(final Server server) { + super.configureServer(server); configureSslServerParameters(server); - return server; } protected void configureSslClientParameters(final Client client) { diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java index eabfb38a67..65935aef4a 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/SslGetTestCase.java @@ -29,11 +29,6 @@ */ public class SslGetTestCase extends SslBaseConnectorsTestCase { - @Override - protected boolean shouldDebug() { - return true; - } - @Override protected void doTest(final int serverPort) throws Exception { final Response response = sendGet(format("https://localhost:%d", serverPort)); From 80e948c13cb8a3a22782abd68d6c3f232f35109e Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sat, 1 Mar 2025 20:49:01 +0100 Subject: [PATCH 5/6] Configure server correctly with graceful shutdown --- .../restlet/ext/jetty/JettyServerHelper.java | 14 +- .../test/java/org/restlet/ext/jetty/Lock.java | 21 ++- .../ext/jetty/ShutdownHookTestCase.java | 131 +++++++++++------- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java index 03763a4cfa..d1feffb732 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java @@ -266,6 +266,7 @@ protected abstract ConnectionFactory[] createConnectionFactories( */ private Connector createConnector(org.eclipse.jetty.server.Server server) { final HttpConfiguration configuration = createConfiguration(); + final ConnectionFactory[] connectionFactories = createConnectionFactories( configuration); @@ -399,7 +400,12 @@ private ThreadPool createThreadPool() { threadPool.setMaxThreads(getThreadPoolMaxThreads()); threadPool.setThreadsPriority(getThreadPoolThreadsPriority()); threadPool.setIdleTimeout(getThreadPoolIdleTimeout()); - threadPool.setStopTimeout(getThreadPoolStopTimeout()); + if (getShutdownGracefully()) { + threadPool.setStopTimeout(getThreadPoolStopTimeout()); + } else { + threadPool.setStopTimeout(0); // The thread pool stops immediately. + } + return threadPool; } @@ -520,7 +526,7 @@ public int getConnectorStopTimeout() { */ public int getHttpHeaderCacheSize() { return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.headerCacheSize", "512")); + .getFirstValue("http.headerCacheSize", "1024")); } /** @@ -797,6 +803,8 @@ public void start() throws Exception { + " server on port " + getHelped().getPort()); try { server.start(); + // We won't know the local port until after the server starts + setEphemeralPort(connector.getLocalPort()); } catch (Exception e) { // Make sure that all resources are released, otherwise thread-pool // may still be running. @@ -804,8 +812,6 @@ public void start() throws Exception { throw e; } - // We won't know the local port until after the server starts - setEphemeralPort(connector.getLocalPort()); } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java index 4aab0d1143..ec273f2de0 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/Lock.java @@ -1,19 +1,38 @@ package org.restlet.ext.jetty; import java.time.Duration; +import java.time.Instant; import java.util.concurrent.CountDownLatch; +import java.util.logging.Logger; import static java.util.concurrent.TimeUnit.MILLISECONDS; public class Lock { + private static final Logger LOGGER = Logger.getLogger("Lock"); + private final CountDownLatch lock = new CountDownLatch(1); + private final String name; + + public Lock(String name) { + this.name = name; + } public void unlock() { + log("lock " + name + " unlock"); lock.countDown(); } public boolean awaitForUnlockingFor(final Duration waitTime) throws InterruptedException { + log("lock " + name + " awaitForUnlocking"); + final long waitTimeInMs = waitTime.toMillis(); - return lock.await(waitTimeInMs, MILLISECONDS); + boolean await = lock.await(waitTimeInMs, MILLISECONDS); + log("lock " + name + " awaitForUnlocking done " + await); + return await; } + + private static void log(final String message) { + LOGGER.fine(Instant.now().toString() + " " + message); + } + } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java index de6c170f33..85c13e4171 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/ShutdownHookTestCase.java @@ -24,6 +24,7 @@ package org.restlet.ext.jetty; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.restlet.*; import org.restlet.data.MediaType; @@ -43,9 +44,11 @@ public class ShutdownHookTestCase { private static final Logger LOGGER = Logger.getLogger("ShutdownHookTest"); + private static boolean shouldDebug = false; - static { - LOGGER.setLevel(Level.INFO); + @BeforeAll + private static void setUp() { + LOGGER.setLevel(Level.INFO); } /** @@ -53,16 +56,15 @@ public class ShutdownHookTestCase { */ @Test public void whenServerIsNotHandlingRequestThenItStopsImmediately() throws Exception { - // Given - final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that - final Restlet hangingRestlet = newHangingRestlet(requestHangingTime); - final Duration shutdownTimeout = Duration.ofSeconds(2); - final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + // Given a server resource that takes 1 min to send a response + final Restlet hangingRestlet = newHangingRestlet(Duration.ofMinutes(1)); + // Given a server with a 3-seconds graceful shutdown + final Server server = startServerWithGracefulShutdown(Duration.ofSeconds(3), hangingRestlet); - // When + // When the server stops final Instant serverAskedToStopInstant = stopServer(server); - // Then + // Then the server stops immediately (no pending request) assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); } @@ -73,23 +75,29 @@ public void whenServerIsNotHandlingRequestThenItStopsImmediately() throws Except */ @Test public void whenServerIsHandlingBlockingRequestThenItStopsImmediately() throws Exception { - // Given - final Lock lock = new Lock(); - final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that - final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + // Given a server resource that takes 1 min to send a response + final Lock lock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), lock); + // Given a server without graceful shutdown Server server = startServerWithoutGracefulShutdown(hangingRestlet); + // Given a client that sends a request final TestClient testClient = new TestClient(server); new Thread(testClient).start(); - // When - final boolean isResourceUnlocked = lock.awaitForUnlockingFor(Duration.ofSeconds(2)); + // When we stop the server while there is a pending request + log("before resource unlock"); + final boolean isResourceUnlocked = lock.awaitForUnlockingFor(Duration.ofSeconds(3)); + log("after resource unlock"); + log("before stopping server while request is pending"); final Instant serverAskedToStopInstant = stopServer(server); - final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(Duration.ofSeconds(2)); + log("before client unlock"); + final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(Duration.ofSeconds(4)); + log("after client unlock"); // Then assertTrue(isResourceUnlocked, "The resource didn't receive the request"); - assertTrue(isClientResourceUnlocked, "The client didn't achieved the request"); + assertTrue(isClientResourceUnlocked, "The client didn't achieve the request"); assertTrue(testClient.cr.getStatus().isError(), "The request should have ended in error"); assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, testClient.stoppedAt); assertIntervalBetweenDatesEquals(Duration.ZERO, serverAskedToStopInstant, Instant.now()); @@ -101,30 +109,32 @@ public void whenServerIsHandlingBlockingRequestThenItStopsImmediately() throws E * This is done by making a request froze, then stopping the server, and checking that it waited the expected amount of time before shutting down. */ @Test - public void whenServerIsHandlingBlockingRequestThenItGracefullyWaitsFor2SecondsBeforeStopping() throws Exception { - // Given - final Lock lock = new Lock(); - final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that - final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + public void whenServerIsHandlingBlockingRequestThenItGracefullyWaitsFor1SecondBeforeStopping() throws Exception { + // Given a server resource that takes 1 min to send a response + final Lock serverLock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), serverLock); - final Duration shutdownTimeout = Duration.ofSeconds(2); + // Given a server with a 1-second graceful shutdown + final Duration shutdownTimeout = Duration.ofSeconds(1); final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); - final TestClient testClient = new TestClient(server); - new Thread(testClient).start(); + // Given a client that sends a request + final TestClient hangingClient = new TestClient(server); + new Thread(hangingClient).start(); - // When - final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + // When we stop the server while there is a pending request + final boolean isResourceUnlocked = serverLock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + log("Before ask server to stop"); final Instant serverAskedToStopInstant = stopServer(server); - final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); + log("After ask server to stop"); + final boolean isClientResourceUnlocked = hangingClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); // Then assertTrue(isResourceUnlocked, "The resource didn't receive the request"); assertTrue(isClientResourceUnlocked, "The client didn't achieved the request"); - assertTrue(testClient.cr.getStatus().isError(), "The request should have ended in error"); + assertTrue(hangingClient.cr.getStatus().isError(), "The request should have ended in error"); - Thread.sleep(100000); - assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, testClient.stoppedAt); + assertIntervalBetweenDatesEquals(shutdownTimeout, serverAskedToStopInstant, hangingClient.stoppedAt); assertIntervalBetweenDatesEquals(toJettyEffectiveTimeout(shutdownTimeout), serverAskedToStopInstant, Instant.now()); } @@ -135,14 +145,15 @@ public void whenServerIsHandlingBlockingRequestThenItGracefullyWaitsFor2SecondsB */ @Test public void whenServerIsHandlingBlockingRequestThenItRefusesNewRequest() throws Exception { - // Given - final Lock lock = new Lock(); - final Duration requestHangingTime = Duration.ofMinutes(1); // Test ALWAYS fails before that - final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + // Given a server resource that takes 1 min to send a response + final Lock lock = new Lock("Server"); + final Restlet hangingRestlet = newHangingAndLockedRestlet(Duration.ofMinutes(1), lock); - final Duration shutdownTimeout = Duration.ofSeconds(2); + // Given a server with a 1-second graceful shutdown + final Duration shutdownTimeout = Duration.ofSeconds(1); final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + // Given a client that sends a request final TestClient firstTestClient = new TestClient(server); new Thread(firstTestClient).start(); @@ -171,23 +182,25 @@ public void whenServerIsHandlingBlockingRequestThenItRefusesNewRequest() throws */ @Test public void whenServerIsHandlingLongRequestThenRequestIsHandledCorrectlyBeforeStopping() throws Exception { - // Given - final Lock lock = new Lock(); - final Duration requestHangingTime = Duration.ofSeconds(2); + // Given a server resource that takes 1 sec to send a response + final Lock lock = new Lock("Server"); + Duration requestHangingTime = Duration.ofSeconds(1); final Restlet hangingRestlet = newHangingAndLockedRestlet(requestHangingTime, lock); + // Given a server with a 20-seconds graceful shutdown final Duration shutdownTimeout = Duration.ofSeconds(20); final Server server = startServerWithGracefulShutdown(shutdownTimeout, hangingRestlet); + // Given a client that sends a request final TestClient testClient = new TestClient(server); new Thread(testClient).start(); // When final boolean isResourceUnlocked = lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); final Instant serverAskedToStopInstant = stopServer(server); - LOGGER.fine("Client resource wait lock"); + log("Client resource wait lock"); final boolean isClientResourceUnlocked = testClient.lock.awaitForUnlockingFor(shutdownTimeout.multipliedBy(2)); - LOGGER.fine("Client resource unlocked"); + log("Client resource unlocked"); // Then assertTrue(isResourceUnlocked, "The resource didn't receive the request"); @@ -206,7 +219,7 @@ private static void assertIntervalBetweenDatesEquals(final Duration expectedDura .abs() .minus(tolerance) .isNegative(); - assertTrue(isDateDifferenceNearlyEqualToExpectedDuration, String.format("Expected delay: %d second(s) versus %d second(s)\n", expectedDuration.getSeconds(), dateDifference.getSeconds())); + assertTrue(isDateDifferenceNearlyEqualToExpectedDuration, String.format("Expected delay: %d second(s) versus %d second(s)\n", expectedDuration.toMillis(), dateDifference.toMillis())); } private static void assertIntervalBetweenDatesIsLessThan(final Duration expectedDuration, final Instant firstInstant, final Instant secondInstant) { @@ -234,14 +247,22 @@ private Server startServer(final boolean graceful, final Duration timeout, final // 0 port means it will be computed when the server starts Server server = new Server(new Context(), singletonList(Protocol.HTTP), null, 0, restlet, HttpServerHelper.class.getCanonicalName()); + if (shouldDebug) { + server.getContext().getParameters().add("tracing", "true"); + System.setProperty("org.eclipse.jetty.LEVEL", "TRACE"); + System.setProperty("sun.net.www.protocol.http.HttpURLConnection.LEVEL", "ALL"); + Engine.setLogLevel(Level.FINE); + } + if (graceful) { + // Don't let the lowResource monitor mess with the current test server.getContext().getParameters().add("lowResource.idleTimeout", Long.toString(timeout.toMillis() * 10)); } server.getContext().getParameters().add("shutdown.gracefully", Boolean.toString(graceful)); server.getContext().getParameters().add("shutdown.timeout", Long.toString(timeout.toMillis())); server.start(); - LOGGER.fine( "Server started on port " + server.getEphemeralPort()); + log( "Server started on port " + server.getEphemeralPort()); return server; } @@ -253,16 +274,16 @@ private static Restlet newHangingAndLockedRestlet(final Duration requestHangingT return new Restlet() { @Override public void handle(final Request request, final Response response) { - LOGGER.fine("Restlet opens lock"); + log("Restlet opens lock"); lock.unlock(); - LOGGER.fine("Restlet starts sleeping"); + log("Restlet starts sleeping"); try { Thread.sleep(requestHangingTime.toMillis()); } catch (Exception e) { // silently stops, especially when Jetty server will abruptly quit after time out LOGGER.log(Level.FINE, "Restlet error", e); } - LOGGER.fine("Restlet woke up, answering"); + log("Restlet woke up, answering"); response.setEntity("hello, world", MediaType.TEXT_ALL); } }; @@ -296,7 +317,7 @@ private static class TestClient implements Runnable { public TestClient(final Server server) { cr = new ClientResource("http://localhost:" + server.getEphemeralPort()); cr.setRetryOnError(false); - this.lock = new Lock(); + this.lock = new Lock("TestClient"); } @Override @@ -315,15 +336,15 @@ public void run() { } private synchronized Instant stopServer(final Server server) { - LOGGER.log(Level.FINE, "Server stopping"); + log("Server stopping"); Instant serverAskedToStopInstant = Instant.now(); try { final HttpServerHelper serverHelper = (HttpServerHelper) server.getContext().getAttributes().get("org.restlet.engine.helper"); serverHelper.getWrappedServer().stop(); - LOGGER.log(Level.FINE, "Server stopped"); + log("Server stopped"); } catch (Exception e) { // silently ignore errors - LOGGER.log(Level.FINE, "Server stopped", e); + log("Server stopped", e); } return serverAskedToStopInstant; } @@ -332,7 +353,15 @@ private synchronized Instant stopServer(final Server server) { * Returns the effective Jetty timeout since there is an extra half-timeout in the {@link org.eclipse.jetty.util.thread.QueuedThreadPool}. */ private Duration toJettyEffectiveTimeout(final Duration timeout) { - return timeout.multipliedBy(3).dividedBy(2); // FIXME: needs improvements + return timeout.plusMillis(500); // FIXME: needs improvements + } + + private static void log(final String message) { + LOGGER.info(Instant.now().toString() + " " + message); + } + + private static void log(final String message, final Exception exception) { + LOGGER.log(Level.INFO, Instant.now().toString() + " " + message, exception); } } From f6c46c7a83665d063b0344cbfb552b06ba5a3e96 Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Sat, 1 Mar 2025 20:57:44 +0100 Subject: [PATCH 6/6] Restore RedirectTestCase --- .../test/java/org/restlet/routing/RedirectTestCase.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java index 6e6d755e71..57aa444395 100644 --- a/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java +++ b/org.restlet.java/org.restlet/src/test/java/org/restlet/routing/RedirectTestCase.java @@ -15,8 +15,8 @@ import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.representation.StringRepresentation; -import org.restlet.routing.Redirector; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertNotNull; /** @@ -39,7 +39,7 @@ private void testCall(Context context, Method method, String uri) /** * Tests the cookies parsing. */ - // @Test TODO why does it fail in CI? + @Test public void testRedirect() throws Exception { // Create components final Component clientComponent = new Component(); @@ -84,12 +84,11 @@ public void handle(Request request, Response response) { // Tests final Context context = clientComponent.getContext(); - String uri = "http://localhost:" + TEST_PORT + "/?foo=bar"; + String uri = format("http://localhost:%d/?foo=bar", TEST_PORT); testCall(context, Method.GET, uri); testCall(context, Method.DELETE, uri); - uri = "http://localhost:" + TEST_PORT - + "/abcd/efgh/ijkl?foo=bar&foo=beer"; + uri = format("http://localhost:%d/abcd/efgh/ijkl?foo=bar&foo=beer", TEST_PORT); testCall(context, Method.GET, uri); testCall(context, Method.DELETE, uri);