diff --git a/.gitignore b/.gitignore index b023787595167bf82805d1b90e2bb5b6a4ae23d8..d424b2597a2c49be2f089e39103dbd4dded8d276 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ test-output MANIFEST.MF work atlassian-ide-plugin.xml +/bom/.flattened-pom.xml diff --git a/.travis.yml b/.travis.yml index bb8adf60b089fcab4d4ae7c3fd134d3e9541a741..2760c26e6f836c6fd1063db01acccd5df173e63e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: java jdk: - - oraclejdk8 + - openjdk8 before_script: - travis/before_script.sh script: - - mvn test -Ptest-output + - mvn test javadoc:javadoc -Ptest-output - find $HOME/.m2 -name "_remote.repositories" | xargs rm - find $HOME/.m2 -name "resolver-status.properties" | xargs rm -f @@ -16,12 +16,6 @@ after_success: sudo: false -# https://github.com/travis-ci/travis-ci/issues/3259 -addons: - apt: - packages: - - oracle-java8-installer - # Cache settings cache: directories: diff --git a/README.md b/README.md index eed94ccf15dc31e99efb85c8b18848aae897b18f..a306c4937bee18680d8df9408190fdee7df6322f 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,41 @@ Follow [@AsyncHttpClient](https://twitter.com/AsyncHttpClient) on Twitter. The AsyncHttpClient (AHC) library allows Java applications to easily execute HTTP requests and asynchronously process HTTP responses. The library also supports the WebSocket Protocol. -It's built on top of [Netty](https://github.com/netty/netty). I's currently compiled on Java 8 but runs on Java 9 too. +It's built on top of [Netty](https://github.com/netty/netty). It's currently compiled on Java 8 but runs on Java 9 too. ## Installation -Binaries are deployed on Maven central: +Binaries are deployed on Maven Central. + +Import the AsyncHttpClient Bill of Materials (BOM) to add dependency management for AsyncHttpClient artifacts to your project: + +```xml +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-bom</artifactId> + <version>LATEST_VERSION</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> +``` + +Add a dependency on the main AsyncHttpClient artifact: ```xml -<dependency> - <groupId>org.asynchttpclient</groupId> - <artifactId>async-http-client</artifactId> - <version>LATEST_VERSION</version> -</dependency> +<dependencies> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client</artifactId> + </dependency> +</dependencies> ``` +The `async-http-client-extras-*` and other modules can also be added without having to specify the version for each dependency, because they are all managed via the BOM. + ## Version AHC doesn't use SEMVER, and won't. @@ -73,7 +94,7 @@ AsyncHttpClient c = asyncHttpClient(config().setProxyServer(proxyServer("127.0.0 ### Basics AHC provides 2 APIs for defining requests: bound and unbound. -`AsyncHttpClient` and Dls` provide methods for standard HTTP methods (POST, PUT, etc) but you can also pass a custom one. +`AsyncHttpClient` and Dsl` provide methods for standard HTTP methods (POST, PUT, etc) but you can also pass a custom one. ```java import org.asynchttpclient.*; @@ -159,7 +180,7 @@ See `AsyncCompletionHandler` implementation as an example. The below sample just capture the response status and skips processing the response body chunks. -Note that returning `ABORT` closed the underlying connection. +Note that returning `ABORT` closes the underlying connection. ```java import static org.asynchttpclient.Dsl.*; @@ -196,7 +217,7 @@ Integer statusCode = whenStatusCode.get(); #### Using Continuations -`ListenableFuture` has a `toCompletableFuture` that returns a `CompletableFuture`. +`ListenableFuture` has a `toCompletableFuture` method that returns a `CompletableFuture`. Beware that canceling this `CompletableFuture` won't properly cancel the ongoing request. There's a very good chance we'll return a `CompletionStage` instead in the next release. @@ -244,7 +265,7 @@ WebSocket websocket = c.prepareGet("ws://demos.kaazing.com/echo") ## Reactive Streams -AsyncHttpClient has build in support for reactive streams. +AsyncHttpClient has built-in support for reactive streams. You can pass a request body as a `Publisher<ByteBuf>` or a `ReactiveStreamsBodyGenerator`. @@ -289,7 +310,7 @@ Keep up to date on the library development by joining the Asynchronous HTTP Clie Of course, Pull Requests are welcome. -Here a the few rules we'd like you to respect if you do so: +Here are the few rules we'd like you to respect if you do so: * Only edit the code related to the suggested change, so DON'T automatically format the classes you've edited. * Use IntelliJ default formatting rules. diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4a4438aa986f4f4bd634f709248772f6d004eaf --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-project</artifactId> + <version>2.11.0</version> + </parent> + + <artifactId>async-http-client-bom</artifactId> + <packaging>pom</packaging> + <name>Asynchronous Http Client Bill of Materials (BOM)</name> + <description>Importing this BOM will provide dependency management for all AsyncHttpClient artifacts.</description> + <url>http://github.com/AsyncHttpClient/async-http-client/bom</url> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-example</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-guava</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-jdeferred</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-registry</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-retrofit2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-rxjava</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-rxjava2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-simple</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-extras-typesafe-config</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.asynchttpclient</groupId> + <artifactId>async-http-client-netty-utils</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <plugins> + <plugin> + <!-- This plugins allows using a parent for this file, so project.version can be resolved. + A clean BOM without a reference to a parent is generated, so that transitive dependencies will + not be included and cannot interfere with projects that use this BOM. --> + <groupId>org.codehaus.mojo</groupId> + <artifactId>flatten-maven-plugin</artifactId> + <version>1.1.0</version> + <inherited>false</inherited> + <executions> + <execution> + <id>flatten</id> + <phase>process-resources</phase> + <goals> + <goal>flatten</goal> + </goals> + <configuration> + <flattenMode>bom</flattenMode> + <pomElements> + <properties>remove</properties> + <dependencies>remove</dependencies> + <build>remove</build> + </pomElements> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/client/pom.xml b/client/pom.xml index 6a46b6a7a2b9bd80d6e7d6639393a834f5a22265..ee341ce4ba3022fe6d8a36257a70378c98956f7a 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,13 +2,17 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-project</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client</artifactId> <name>Asynchronous Http Client</name> <description>The Async Http Client (AHC) classes.</description> + <properties> + <javaModuleName>org.asynchttpclient.client</javaModuleName> + </properties> + <build> <plugins> <plugin> @@ -53,7 +57,8 @@ </dependency> <dependency> <groupId>io.netty</groupId> - <artifactId>netty-resolver-dns</artifactId> + <artifactId>netty-transport-native-kqueue</artifactId> + <classifier>osx-x86_64</classifier> </dependency> <dependency> <groupId>org.reactivestreams</groupId> diff --git a/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java b/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java index c7b60e0b4cb15bcf3341d2888a5e5374c34e42cc..d1f30c1ac35548af88db050cba8048b2ddf6bb48 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java +++ b/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory; /** * An {@link AsyncHandler} augmented with an {@link #onCompleted(Response)} * convenience method which gets called when the {@link Response} processing is - * finished. This class also implement the {@link ProgressAsyncHandler} + * finished. This class also implements the {@link ProgressAsyncHandler} * callback, all doing nothing except returning * {@link org.asynchttpclient.AsyncHandler.State#CONTINUE} * diff --git a/client/src/main/java/org/asynchttpclient/AsyncHandler.java b/client/src/main/java/org/asynchttpclient/AsyncHandler.java index 090503cf1422747ab7cd18e236b5a5c9555e9c9b..6733c947112395c441f3b10c9403c67246eb4275 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHandler.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHandler.java @@ -19,6 +19,7 @@ import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import org.asynchttpclient.netty.request.NettyRequest; +import javax.net.ssl.SSLSession; import java.net.InetSocketAddress; import java.util.List; @@ -37,9 +38,9 @@ import java.util.List; * </ol> * <br> * Returning a {@link AsyncHandler.State#ABORT} from any of those callback methods will interrupt asynchronous response - * processing, after that only {@link #onCompleted()} is going to be called. + * processing. After that, only {@link #onCompleted()} is going to be called. * <br> - * AsyncHandler aren't thread safe, hence you should avoid re-using the same instance when doing concurrent requests. + * AsyncHandlers aren't thread safe. Hence, you should avoid re-using the same instance when doing concurrent requests. * As an example, the following may produce unexpected results: * <blockquote><pre> * AsyncHandler ah = new AsyncHandler() {....}; @@ -49,9 +50,10 @@ import java.util.List; * </pre></blockquote> * It is recommended to create a new instance instead. * <p> - * Do NOT perform any blocking operation in there, typically trying to send another request and call get() on its future. + * Do NOT perform any blocking operations in any of these methods. A typical example would be trying to send another + * request and calling get() on its future. * There's a chance you might end up in a dead lock. - * If you really to perform blocking operation, executed it in a different dedicated thread pool. + * If you really need to perform a blocking operation, execute it in a different dedicated thread pool. * * @param <T> Type of object returned by the {@link java.util.concurrent.Future#get} */ @@ -142,6 +144,8 @@ public interface AsyncHandler<T> { default void onHostnameResolutionFailure(String name, Throwable cause) { } + // ////////////// TCP CONNECT //////// + /** * Notify the callback when trying to open a new connection. * <p> @@ -152,8 +156,6 @@ public interface AsyncHandler<T> { default void onTcpConnectAttempt(InetSocketAddress remoteAddress) { } - // ////////////// TCP CONNECT //////// - /** * Notify the callback after a successful connect * @@ -174,18 +176,18 @@ public interface AsyncHandler<T> { default void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { } + // ////////////// TLS /////////////// + /** * Notify the callback before TLS handshake */ default void onTlsHandshakeAttempt() { } - // ////////////// TLS /////////////// - /** * Notify the callback after the TLS was successful */ - default void onTlsHandshakeSuccess() { + default void onTlsHandshakeSuccess(SSLSession sslSession) { } /** @@ -196,14 +198,14 @@ public interface AsyncHandler<T> { default void onTlsHandshakeFailure(Throwable cause) { } + // /////////// POOLING ///////////// + /** * Notify the callback when trying to fetch a connection from the pool. */ default void onConnectionPoolAttempt() { } - // /////////// POOLING ///////////// - /** * Notify the callback when a new connection was successfully fetched from the pool. * @@ -220,6 +222,8 @@ public interface AsyncHandler<T> { default void onConnectionOffer(Channel connection) { } + // //////////// SENDING ////////////// + /** * Notify the callback when a request is being written on the channel. If the original request causes multiple requests to be sent, for example, because of authorization or * retry, it will be notified multiple times. @@ -229,8 +233,6 @@ public interface AsyncHandler<T> { default void onRequestSend(NettyRequest request) { } - // //////////// SENDING ////////////// - /** * Notify the callback every time a request is being retried. */ diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java index 1510de4513d06eb350a77ee78b799ffec8d63d19..2ab335f3f6a199dba2ca501c69929b473ec67d5f 100755 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java @@ -21,15 +21,15 @@ import java.util.concurrent.Future; import java.util.function.Predicate; /** - * This class support asynchronous and synchronous HTTP request. + * This class support asynchronous and synchronous HTTP requests. * <br> - * To execute synchronous HTTP request, you just need to do + * To execute a synchronous HTTP request, you just need to do * <blockquote><pre> * AsyncHttpClient c = new AsyncHttpClient(); * Future<Response> f = c.prepareGet(TARGET_URL).execute(); * </pre></blockquote> * <br> - * The code above will block until the response is fully received. To execute asynchronous HTTP request, you + * The code above will block until the response is fully received. To execute an asynchronous HTTP request, you * create an {@link AsyncHandler} or its abstract implementation, {@link AsyncCompletionHandler} * <br> * <blockquote><pre> @@ -48,7 +48,7 @@ import java.util.function.Predicate; * }); * Response response = f.get(); * - * // We are just interested to retrieve the status code. + * // We are just interested in retrieving the status code. * Future<Integer> f = c.prepareGet(TARGET_URL).execute(new AsyncCompletionHandler<Integer>() { * * @Override @@ -63,10 +63,10 @@ import java.util.function.Predicate; * }); * Integer statusCode = f.get(); * </pre></blockquote> - * The {@link AsyncCompletionHandler#onCompleted(Response)} will be invoked once the http response has been fully read, which include - * the http headers and the response body. Note that the entire response will be buffered in memory. + * The {@link AsyncCompletionHandler#onCompleted(Response)} method will be invoked once the http response has been fully read. + * The {@link Response} object includes the http headers and the response body. Note that the entire response will be buffered in memory. * <br> - * You can also have more control about the how the response is asynchronously processed by using a {@link AsyncHandler} + * You can also have more control about the how the response is asynchronously processed by using an {@link AsyncHandler} * <blockquote><pre> * AsyncHttpClient c = new AsyncHttpClient(); * Future<String> f = c.prepareGet(TARGET_URL).execute(new AsyncHandler<String>() { @@ -106,8 +106,8 @@ import java.util.function.Predicate; * * String bodyResponse = f.get(); * </pre></blockquote> - * You can asynchronously process the response status,headers and body and decide when to - * stop the processing the response by returning a new {@link AsyncHandler.State#ABORT} at any moment. + * You can asynchronously process the response status, headers and body and decide when to + * stop processing the response by returning a new {@link AsyncHandler.State#ABORT} at any moment. * * This class can also be used without the need of {@link AsyncHandler}. * <br> @@ -125,8 +125,8 @@ import java.util.function.Predicate; * Response r = f.get(); * </pre></blockquote> * <br> - * An instance of this class will cache every HTTP 1.1 connections and close them when the {@link DefaultAsyncHttpClientConfig#getReadTimeout()} - * expires. This object can hold many persistent connections to different host. + * An instance of this class will cache every HTTP 1.1 connection and close them when the {@link DefaultAsyncHttpClientConfig#getReadTimeout()} + * expires. This object can hold many persistent connections to different hosts. */ public interface AsyncHttpClient extends Closeable { @@ -138,7 +138,7 @@ public interface AsyncHttpClient extends Closeable { boolean isClosed(); /** - * Set default signature calculator to use for requests build by this client instance + * Set default signature calculator to use for requests built by this client instance * * @param signatureCalculator a signature calculator * @return {@link RequestBuilder} diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index e0f8413662ba344ea24e0fd477c5cf9113cbfc93..391014df7bfe1989ab8e1d22a0e11701f4530224 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -65,6 +65,14 @@ public interface AsyncHttpClientConfig { */ int getMaxConnectionsPerHost(); + /** + * Return the maximum duration in milliseconds an {@link AsyncHttpClient} can wait to acquire a free channel + * + * @return Return the maximum duration in milliseconds an {@link AsyncHttpClient} can wait to acquire a free channel + */ + int getAcquireFreeChannelTimeout(); + + /** * Return the maximum time in millisecond an {@link AsyncHttpClient} can wait when connecting to a remote host * @@ -310,6 +318,8 @@ public interface AsyncHttpClientConfig { boolean isSoReuseAddress(); + boolean isSoKeepAlive(); + int getSoLinger(); int getSoSndBuf(); diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java index 8d2c3f7ab10fb7ec9cde6e760870643bcd4a2216..7e8c21f901d38b4ada80ccdb7a708475faad2e4f 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java @@ -194,7 +194,7 @@ public class DefaultAsyncHttpClient implements AsyncHttpClient { try { List<Cookie> cookies = config.getCookieStore().get(request.getUri()); if (!cookies.isEmpty()) { - RequestBuilder requestBuilder = new RequestBuilder(request); + RequestBuilder requestBuilder = request.toBuilder(); for (Cookie cookie : cookies) { requestBuilder.addOrReplaceCookie(cookie); } @@ -264,7 +264,7 @@ public class DefaultAsyncHttpClient implements AsyncHttpClient { } if (request.getRangeOffset() != 0) { - RequestBuilder builder = new RequestBuilder(request); + RequestBuilder builder = request.toBuilder(); builder.setHeader("Range", "bytes=" + request.getRangeOffset() + "-"); request = builder.build(); } diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 96d95f91bd0424640c281b54034a80cda8e37e7c..f179f08a33e98ae20b25938422d5de6d6dc28144 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -84,6 +84,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final int connectionTtl; private final int maxConnections; private final int maxConnectionsPerHost; + private final int acquireFreeChannelTimeout; private final ChannelPool channelPool; private final ConnectionSemaphoreFactory connectionSemaphoreFactory; private final KeepAliveStrategy keepAliveStrategy; @@ -122,6 +123,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final ByteBufAllocator allocator; private final boolean tcpNoDelay; private final boolean soReuseAddress; + private final boolean soKeepAlive; private final int soLinger; private final int soSndBuf; private final int soRcvBuf; @@ -163,6 +165,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { int connectionTtl, int maxConnections, int maxConnectionsPerHost, + int acquireFreeChannelTimeout, ChannelPool channelPool, ConnectionSemaphoreFactory connectionSemaphoreFactory, KeepAliveStrategy keepAliveStrategy, @@ -191,6 +194,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { // tuning boolean tcpNoDelay, boolean soReuseAddress, + boolean soKeepAlive, int soLinger, int soSndBuf, int soRcvBuf, @@ -250,6 +254,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { this.connectionTtl = connectionTtl; this.maxConnections = maxConnections; this.maxConnectionsPerHost = maxConnectionsPerHost; + this.acquireFreeChannelTimeout = acquireFreeChannelTimeout; this.channelPool = channelPool; this.connectionSemaphoreFactory = connectionSemaphoreFactory; this.keepAliveStrategy = keepAliveStrategy; @@ -278,6 +283,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { // tuning this.tcpNoDelay = tcpNoDelay; this.soReuseAddress = soReuseAddress; + this.soKeepAlive = soKeepAlive; this.soLinger = soLinger; this.soSndBuf = soSndBuf; this.soRcvBuf = soRcvBuf; @@ -445,6 +451,9 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { return maxConnectionsPerHost; } + @Override + public int getAcquireFreeChannelTimeout() { return acquireFreeChannelTimeout; } + @Override public ChannelPool getChannelPool() { return channelPool; @@ -554,6 +563,11 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { return soReuseAddress; } + @Override + public boolean isSoKeepAlive() { + return soKeepAlive; + } + @Override public int getSoLinger() { return soLinger; @@ -696,6 +710,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private int connectionTtl = defaultConnectionTtl(); private int maxConnections = defaultMaxConnections(); private int maxConnectionsPerHost = defaultMaxConnectionsPerHost(); + private int acquireFreeChannelTimeout = defaultAcquireFreeChannelTimeout(); private ChannelPool channelPool; private ConnectionSemaphoreFactory connectionSemaphoreFactory; private KeepAliveStrategy keepAliveStrategy = new DefaultKeepAliveStrategy(); @@ -719,6 +734,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { // tuning private boolean tcpNoDelay = defaultTcpNoDelay(); private boolean soReuseAddress = defaultSoReuseAddress(); + private boolean soKeepAlive = defaultSoKeepAlive(); private int soLinger = defaultSoLinger(); private int soSndBuf = defaultSoSndBuf(); private int soRcvBuf = defaultSoRcvBuf(); @@ -801,6 +817,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { // tuning tcpNoDelay = config.isTcpNoDelay(); soReuseAddress = config.isSoReuseAddress(); + soKeepAlive = config.isSoKeepAlive(); soLinger = config.getSoLinger(); soSndBuf = config.getSoSndBuf(); soRcvBuf = config.getSoRcvBuf(); @@ -991,6 +1008,16 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { return this; } + /** + * Sets the maximum duration in milliseconds to acquire a free channel to send a request + * @param acquireFreeChannelTimeout maximum duration in milliseconds to acquire a free channel to send a request + * @return the same builder instance + */ + public Builder setAcquireFreeChannelTimeout(int acquireFreeChannelTimeout) { + this.acquireFreeChannelTimeout = acquireFreeChannelTimeout; + return this; + } + public Builder setChannelPool(ChannelPool channelPool) { this.channelPool = channelPool; return this; @@ -1110,6 +1137,11 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { return this; } + public Builder setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + return this; + } + public Builder setSoLinger(int soLinger) { this.soLinger = soLinger; return this; @@ -1249,6 +1281,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { connectionTtl, maxConnections, maxConnectionsPerHost, + acquireFreeChannelTimeout, channelPool, connectionSemaphoreFactory, keepAliveStrategy, @@ -1269,6 +1302,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { cookieStore, tcpNoDelay, soReuseAddress, + soKeepAlive, soLinger, soSndBuf, soRcvBuf, diff --git a/client/src/main/java/org/asynchttpclient/Request.java b/client/src/main/java/org/asynchttpclient/Request.java index 0bcf3ae710a21edf75938b287f5398e806210ad9..cf6a82dee29591ff21ec3df16e1808d728805a48 100644 --- a/client/src/main/java/org/asynchttpclient/Request.java +++ b/client/src/main/java/org/asynchttpclient/Request.java @@ -180,4 +180,12 @@ public interface Request { * @return the NameResolver to be used to resolve hostnams's IP */ NameResolver<InetAddress> getNameResolver(); + + /** + * @return a new request builder using this request as a prototype + */ + @SuppressWarnings("deprecation") + default RequestBuilder toBuilder() { + return new RequestBuilder(this); + } } diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilder.java b/client/src/main/java/org/asynchttpclient/RequestBuilder.java index ad0a141495c10ee8b4129f9ad73a43f41a56d611..4761f0c2c4b28c5275a7590ef4fbd3563efc61ff 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilder.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilder.java @@ -18,7 +18,7 @@ package org.asynchttpclient; import static org.asynchttpclient.util.HttpConstants.Methods.GET; /** - * Builder for a {@link Request}. Warning: mutable and not thread-safe! Beware that it holds a reference on the Request instance it builds, so modifying the builder will modify the + * Builder for a {@link Request}. Warning: mutable and not thread-safe! Beware that it holds a reference to the Request instance it builds, so modifying the builder will modify the * request even after it has been built. */ public class RequestBuilder extends RequestBuilderBase<RequestBuilder> { @@ -39,10 +39,15 @@ public class RequestBuilder extends RequestBuilderBase<RequestBuilder> { super(method, disableUrlEncoding, validateHeaders); } + /** + * @deprecated Use request.toBuilder() instead + */ + @Deprecated public RequestBuilder(Request prototype) { super(prototype); } + @Deprecated public RequestBuilder(Request prototype, boolean disableUrlEncoding, boolean validateHeaders) { super(prototype, disableUrlEncoding, validateHeaders); } diff --git a/client/src/main/java/org/asynchttpclient/Response.java b/client/src/main/java/org/asynchttpclient/Response.java index 51fa5ac30ae5ef3202f24b59f5bed1c00b693f5e..99f033e995345d108a6557c16c6cf858cd1a7335 100644 --- a/client/src/main/java/org/asynchttpclient/Response.java +++ b/client/src/main/java/org/asynchttpclient/Response.java @@ -160,16 +160,16 @@ public interface Response { boolean hasResponseBody(); /** - * Get remote address client initiated request to. + * Get the remote address that the client initiated the request to. * - * @return remote address client initiated request to, may be {@code null} if asynchronous provider is unable to provide the remote address + * @return The remote address that the client initiated the request to. May be {@code null} if asynchronous provider is unable to provide the remote address */ SocketAddress getRemoteAddress(); /** - * Get local address client initiated request from. + * Get the local address that the client initiated the request from. * - * @return local address client initiated request from, may be {@code null} if asynchronous provider is unable to provide the local address + * @return The local address that the client initiated the request from. May be {@code null} if asynchronous provider is unable to provide the local address */ SocketAddress getLocalAddress(); diff --git a/client/src/main/java/org/asynchttpclient/SslEngineFactory.java b/client/src/main/java/org/asynchttpclient/SslEngineFactory.java index 1157e499f38ce61abb979766b036050a1b107cc0..008f1c7ee845906fe500dab676b3b345f012e5df 100644 --- a/client/src/main/java/org/asynchttpclient/SslEngineFactory.java +++ b/client/src/main/java/org/asynchttpclient/SslEngineFactory.java @@ -19,7 +19,7 @@ import javax.net.ssl.SSLException; public interface SslEngineFactory { /** - * Creates new {@link SSLEngine}. + * Creates a new {@link SSLEngine}. * * @param config the client config * @param peerHost the peer hostname @@ -39,4 +39,12 @@ public interface SslEngineFactory { default void init(AsyncHttpClientConfig config) throws SSLException { // no op } + + /** + * Perform any necessary cleanup. + */ + default void destroy() { + // no op + } + } diff --git a/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java b/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java index b9fb306cf3d0c8c1fdd790e7d9738568754d5a9f..f1c6a5f42f971867a6cd1059b39c560dce9bad0b 100644 --- a/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java +++ b/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java @@ -5,6 +5,8 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpUtil; import org.asynchttpclient.Request; +import java.net.InetSocketAddress; + import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; /** @@ -16,7 +18,7 @@ public class DefaultKeepAliveStrategy implements KeepAliveStrategy { * Implemented in accordance with RFC 7230 section 6.1 https://tools.ietf.org/html/rfc7230#section-6.1 */ @Override - public boolean keepAlive(Request ahcRequest, HttpRequest request, HttpResponse response) { + public boolean keepAlive(InetSocketAddress remoteAddress, Request ahcRequest, HttpRequest request, HttpResponse response) { return HttpUtil.isKeepAlive(response) && HttpUtil.isKeepAlive(request) // support non standard Proxy-Connection diff --git a/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java b/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java index 4d619f222c1045a9c740e3f424f8fe1da75cef9d..c748fe76ac39d0c4b2268df2131bd0315229ad8f 100644 --- a/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java +++ b/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java @@ -17,15 +17,18 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import org.asynchttpclient.Request; +import java.net.InetSocketAddress; + public interface KeepAliveStrategy { /** * Determines whether the connection should be kept alive after this HTTP message exchange. * - * @param ahcRequest the Request, as built by AHC - * @param nettyRequest the HTTP request sent to Netty - * @param nettyResponse the HTTP response received from Netty + * @param remoteAddress the remote InetSocketAddress associated with the request + * @param ahcRequest the Request, as built by AHC + * @param nettyRequest the HTTP request sent to Netty + * @param nettyResponse the HTTP response received from Netty * @return true if the connection should be kept alive, false if it should be closed. */ - boolean keepAlive(Request ahcRequest, HttpRequest nettyRequest, HttpResponse nettyResponse); + boolean keepAlive(InetSocketAddress remoteAddress, Request ahcRequest, HttpRequest nettyRequest, HttpResponse nettyResponse); } diff --git a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java index 274537a6ad4159dd470a5935b8ccf512200bb047..c9e85ca491d18ef96f4aa08a3b148ef1a6551475 100644 --- a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java +++ b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java @@ -22,6 +22,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String THREAD_POOL_NAME_CONFIG = "threadPoolName"; public static final String MAX_CONNECTIONS_CONFIG = "maxConnections"; public static final String MAX_CONNECTIONS_PER_HOST_CONFIG = "maxConnectionsPerHost"; + public static final String ACQUIRE_FREE_CHANNEL_TIMEOUT = "acquireFreeChannelTimeout"; public static final String CONNECTION_TIMEOUT_CONFIG = "connectTimeout"; public static final String POOLED_CONNECTION_IDLE_TIMEOUT_CONFIG = "pooledConnectionIdleTimeout"; public static final String CONNECTION_POOL_CLEANER_PERIOD_CONFIG = "connectionPoolCleanerPeriod"; @@ -39,7 +40,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String USE_PROXY_PROPERTIES_CONFIG = "useProxyProperties"; public static final String VALIDATE_RESPONSE_HEADERS_CONFIG = "validateResponseHeaders"; public static final String AGGREGATE_WEBSOCKET_FRAME_FRAGMENTS_CONFIG = "aggregateWebSocketFrameFragments"; - public static final String ENABLE_WEBSOCKET_COMPRESSION_CONFIG= "enableWebSocketCompression"; + public static final String ENABLE_WEBSOCKET_COMPRESSION_CONFIG = "enableWebSocketCompression"; public static final String STRICT_302_HANDLING_CONFIG = "strict302Handling"; public static final String KEEP_ALIVE_CONFIG = "keepAlive"; public static final String MAX_REQUEST_RETRY_CONFIG = "maxRequestRetry"; @@ -52,6 +53,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String SSL_SESSION_TIMEOUT_CONFIG = "sslSessionTimeout"; public static final String TCP_NO_DELAY_CONFIG = "tcpNoDelay"; public static final String SO_REUSE_ADDRESS_CONFIG = "soReuseAddress"; + public static final String SO_KEEP_ALIVE_CONFIG = "soKeepAlive"; public static final String SO_LINGER_CONFIG = "soLinger"; public static final String SO_SND_BUF_CONFIG = "soSndBuf"; public static final String SO_RCV_BUF_CONFIG = "soRcvBuf"; @@ -97,6 +99,10 @@ public final class AsyncHttpClientConfigDefaults { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + MAX_CONNECTIONS_PER_HOST_CONFIG); } + public static int defaultAcquireFreeChannelTimeout() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + ACQUIRE_FREE_CHANNEL_TIMEOUT); + } + public static int defaultConnectTimeout() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + CONNECTION_TIMEOUT_CONFIG); } @@ -217,6 +223,10 @@ public final class AsyncHttpClientConfigDefaults { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + SO_REUSE_ADDRESS_CONFIG); } + public static boolean defaultSoKeepAlive() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + SO_KEEP_ALIVE_CONFIG); + } + public static int defaultSoLinger() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + SO_LINGER_CONFIG); } diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java index 399638fbb2b9105133d03604c4628968e5f42c3b..d6b671a270eeb7a40142c65befafa495f91b7e3f 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java @@ -198,7 +198,7 @@ public class ResumableAsyncHandler implements AsyncHandler<Response> { byteTransferred.set(resumableListener.length()); } - RequestBuilder builder = new RequestBuilder(request); + RequestBuilder builder = request.toBuilder(); if (request.getHeaders().get(RANGE) == null && byteTransferred.get() != 0) { builder.setHeader(RANGE, "bytes=" + byteTransferred.get() + "-"); } diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java index cb3b05fbd85f49edd9aef9355d88dd5168d811c6..9f84ada7ab7a03c58ec0d6c378e6e1b81aba4d7e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java @@ -90,7 +90,6 @@ public final class NettyResponseFuture<V> implements ListenableFuture<V> { private volatile int isCancelled = 0; private volatile int inAuth = 0; private volatile int inProxyAuth = 0; - private volatile int statusReceived = 0; @SuppressWarnings("unused") private volatile int contentProcessed = 0; @SuppressWarnings("unused") @@ -539,7 +538,6 @@ public final class NettyResponseFuture<V> implements ListenableFuture<V> { ",\n\tredirectCount=" + redirectCount + // ",\n\ttimeoutsHolder=" + TIMEOUTS_HOLDER_FIELD.get(this) + // ",\n\tinAuth=" + inAuth + // - ",\n\tstatusReceived=" + statusReceived + // ",\n\ttouch=" + touch + // '}'; } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 6a5ed0597219f3318604593298319c34228d233c..cf14d3a10141657a0c53f2d85fe0b536da26ff81 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -16,10 +16,12 @@ package org.asynchttpclient.netty.channel; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.*; +import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.ChannelGroupFuture; import io.netty.channel.group.DefaultChannelGroup; +import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.oio.OioEventLoopGroup; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; @@ -36,6 +38,7 @@ import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.resolver.NameResolver; import io.netty.util.Timer; import io.netty.util.concurrent.*; +import io.netty.util.internal.PlatformDependent; import org.asynchttpclient.*; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.ChannelPoolPartitioning; @@ -119,31 +122,31 @@ public class ChannelManager { // check if external EventLoopGroup is defined ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory(config.getThreadPoolName()); allowReleaseEventLoopGroup = config.getEventLoopGroup() == null; - ChannelFactory<? extends Channel> channelFactory; + TransportFactory<? extends Channel, ? extends EventLoopGroup> transportFactory; if (allowReleaseEventLoopGroup) { if (config.isUseNativeTransport()) { - eventLoopGroup = newEpollEventLoopGroup(config.getIoThreadsCount(), threadFactory); - channelFactory = getEpollSocketChannelFactory(); - + transportFactory = getNativeTransportFactory(); } else { - eventLoopGroup = new NioEventLoopGroup(config.getIoThreadsCount(), threadFactory); - channelFactory = NioSocketChannelFactory.INSTANCE; + transportFactory = NioTransportFactory.INSTANCE; } + eventLoopGroup = transportFactory.newEventLoopGroup(config.getIoThreadsCount(), threadFactory); } else { eventLoopGroup = config.getEventLoopGroup(); - if (eventLoopGroup instanceof OioEventLoopGroup) - throw new IllegalArgumentException("Oio is not supported"); if (eventLoopGroup instanceof NioEventLoopGroup) { - channelFactory = NioSocketChannelFactory.INSTANCE; + transportFactory = NioTransportFactory.INSTANCE; + } else if (eventLoopGroup instanceof EpollEventLoopGroup) { + transportFactory = new EpollTransportFactory(); + } else if (eventLoopGroup instanceof KQueueEventLoopGroup) { + transportFactory = new KQueueTransportFactory(); } else { - channelFactory = getEpollSocketChannelFactory(); + throw new IllegalArgumentException("Unknown event loop group " + eventLoopGroup.getClass().getSimpleName()); } } - httpBootstrap = newBootstrap(channelFactory, eventLoopGroup, config); - wsBootstrap = newBootstrap(channelFactory, eventLoopGroup, config); + httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); + wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); // for reactive streams httpBootstrap.option(ChannelOption.AUTO_READ, false); @@ -159,6 +162,7 @@ public class ChannelManager { .option(ChannelOption.ALLOCATOR, config.getAllocator() != null ? config.getAllocator() : ByteBufAllocator.DEFAULT) .option(ChannelOption.TCP_NODELAY, config.isTcpNoDelay()) .option(ChannelOption.SO_REUSEADDR, config.isSoReuseAddress()) + .option(ChannelOption.SO_KEEPALIVE, config.isSoKeepAlive()) .option(ChannelOption.AUTO_CLOSE, false); if (config.getConnectTimeout() > 0) { @@ -184,22 +188,22 @@ public class ChannelManager { return bootstrap; } - private EventLoopGroup newEpollEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { - try { - Class<?> epollEventLoopGroupClass = Class.forName("io.netty.channel.epoll.EpollEventLoopGroup"); - return (EventLoopGroup) epollEventLoopGroupClass.getConstructor(int.class, ThreadFactory.class).newInstance(ioThreadsCount, threadFactory); - } catch (Exception e) { - throw new IllegalArgumentException(e); + @SuppressWarnings("unchecked") + private TransportFactory<? extends Channel, ? extends EventLoopGroup> getNativeTransportFactory() { + String nativeTransportFactoryClassName = null; + if (PlatformDependent.isOsx()) { + nativeTransportFactoryClassName = "org.asynchttpclient.netty.channel.KQueueTransportFactory"; + } else if (!PlatformDependent.isWindows()) { + nativeTransportFactoryClassName = "org.asynchttpclient.netty.channel.EpollTransportFactory"; } - } - @SuppressWarnings("unchecked") - private ChannelFactory<? extends Channel> getEpollSocketChannelFactory() { try { - return (ChannelFactory<? extends Channel>) Class.forName("org.asynchttpclient.netty.channel.EpollSocketChannelFactory").newInstance(); + if (nativeTransportFactoryClassName != null) { + return (TransportFactory<? extends Channel, ? extends EventLoopGroup>) Class.forName(nativeTransportFactoryClassName).newInstance(); + } } catch (Exception e) { - throw new IllegalArgumentException(e); } + throw new IllegalArgumentException("No suitable native transport (epoll or kqueue) available"); } public void configureBootstraps(NettyRequestSender requestSender) { @@ -291,8 +295,9 @@ public class ChannelManager { } private void doClose() { - openChannels.close(); + ChannelGroupFuture groupFuture = openChannels.close(); channelPool.destroy(); + groupFuture.addListener(future -> sslEngineFactory.destroy()); } public void close() { @@ -355,6 +360,11 @@ public class ChannelManager { if (requestUri.isWebSocket()) { pipeline.addAfter(AHC_HTTP_HANDLER, AHC_WS_HANDLER, wsHandler); + + if (config.isEnableWebSocketCompression()) { + pipeline.addBefore(AHC_WS_HANDLER, WS_COMPRESSOR_HANDLER, WebSocketClientCompressionHandler.INSTANCE); + } + pipeline.remove(AHC_HTTP_HANDLER); } return whenHanshaked; @@ -481,7 +491,7 @@ public class ChannelManager { public ClientStats getClientStats() { Map<String, Long> totalConnectionsPerHost = openChannels.stream().map(Channel::remoteAddress).filter(a -> a.getClass() == InetSocketAddress.class) - .map(a -> (InetSocketAddress) a).map(InetSocketAddress::getHostName).collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + .map(a -> (InetSocketAddress) a).map(InetSocketAddress::getHostString).collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); Map<String, Long> idleConnectionsPerHost = channelPool.getIdleChannelCountPerHost(); Map<String, HostStats> statsPerHost = totalConnectionsPerHost.entrySet().stream().collect(Collectors.toMap(Entry::getKey, entry -> { final long totalConnectionCount = entry.getValue(); diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java new file mode 100644 index 0000000000000000000000000000000000000000..04549fd80d449bb1a0ad92024ce0d31823be3787 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * A combined {@link ConnectionSemaphore} with two limits - a global limit and a per-host limit + */ +public class CombinedConnectionSemaphore extends PerHostConnectionSemaphore { + protected final MaxConnectionSemaphore globalMaxConnectionSemaphore; + + CombinedConnectionSemaphore(int maxConnections, int maxConnectionsPerHost, int acquireTimeout) { + super(maxConnectionsPerHost, acquireTimeout); + this.globalMaxConnectionSemaphore = new MaxConnectionSemaphore(maxConnections, acquireTimeout); + } + + @Override + public void acquireChannelLock(Object partitionKey) throws IOException { + long remainingTime = super.acquireTimeout > 0 ? acquireGlobalTimed(partitionKey) : acquireGlobal(partitionKey); + + try { + if (remainingTime < 0 || !getFreeConnectionsForHost(partitionKey).tryAcquire(remainingTime, TimeUnit.MILLISECONDS)) { + releaseGlobal(partitionKey); + throw tooManyConnectionsPerHost; + } + } catch (InterruptedException e) { + releaseGlobal(partitionKey); + throw new RuntimeException(e); + } + } + + protected void releaseGlobal(Object partitionKey) { + this.globalMaxConnectionSemaphore.releaseChannelLock(partitionKey); + } + + protected long acquireGlobal(Object partitionKey) throws IOException { + this.globalMaxConnectionSemaphore.acquireChannelLock(partitionKey); + return 0; + } + + /* + * Acquires the global lock and returns the remaining time, in millis, to acquire the per-host lock + */ + protected long acquireGlobalTimed(Object partitionKey) throws IOException { + long beforeGlobalAcquire = System.currentTimeMillis(); + acquireGlobal(partitionKey); + long lockTime = System.currentTimeMillis() - beforeGlobalAcquire; + return this.acquireTimeout - lockTime; + } + + @Override + public void releaseChannelLock(Object partitionKey) { + this.globalMaxConnectionSemaphore.releaseChannelLock(partitionKey); + super.releaseChannelLock(partitionKey); + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java index 9988421595afba300f105c61a01ec920f63d550c..f9c08b973b2fc8b92f92be35d8ad77c2f72b4200 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java @@ -222,7 +222,7 @@ public final class DefaultChannelPool implements ChannelPool { .map(idle -> idle.getChannel().remoteAddress()) .filter(a -> a.getClass() == InetSocketAddress.class) .map(a -> (InetSocketAddress) a) - .map(InetSocketAddress::getHostName) + .map(InetSocketAddress::getHostString) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java index a102f1def8c38df4998d9c646abf3dc5bda179cf..eba42186eef91245712bc6267c58cc5ccde98fa5 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java @@ -17,14 +17,21 @@ import org.asynchttpclient.AsyncHttpClientConfig; public class DefaultConnectionSemaphoreFactory implements ConnectionSemaphoreFactory { - public ConnectionSemaphore newConnectionSemaphore(AsyncHttpClientConfig config) { - ConnectionSemaphore semaphore = new NoopConnectionSemaphore(); - if (config.getMaxConnections() > 0) { - semaphore = new MaxConnectionSemaphore(config.getMaxConnections()); - } - if (config.getMaxConnectionsPerHost() > 0) { - semaphore = new PerHostConnectionSemaphore(config.getMaxConnectionsPerHost(), semaphore); - } - return semaphore; + public ConnectionSemaphore newConnectionSemaphore(AsyncHttpClientConfig config) { + int acquireFreeChannelTimeout = Math.max(0, config.getAcquireFreeChannelTimeout()); + int maxConnections = config.getMaxConnections(); + int maxConnectionsPerHost = config.getMaxConnectionsPerHost(); + + if (maxConnections > 0 && maxConnectionsPerHost > 0) { + return new CombinedConnectionSemaphore(maxConnections, maxConnectionsPerHost, acquireFreeChannelTimeout); + } + if (maxConnections > 0) { + return new MaxConnectionSemaphore(maxConnections, acquireFreeChannelTimeout); } + if (maxConnectionsPerHost > 0) { + return new CombinedConnectionSemaphore(maxConnections, maxConnectionsPerHost, acquireFreeChannelTimeout); + } + + return new NoopConnectionSemaphore(); + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java similarity index 54% rename from client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java rename to client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java index c6970b6d6c07caf25b31d9c0bb4b540a70c613a4..8f84272916400d0960cfa8b1ceb194bac504477d 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java @@ -13,13 +13,32 @@ */ package org.asynchttpclient.netty.channel; -import io.netty.channel.ChannelFactory; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; -class EpollSocketChannelFactory implements ChannelFactory<EpollSocketChannel> { +import java.util.concurrent.ThreadFactory; + +class EpollTransportFactory implements TransportFactory<EpollSocketChannel, EpollEventLoopGroup> { + + EpollTransportFactory() { + try { + Class.forName("io.netty.channel.epoll.Epoll"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The epoll transport is not available"); + } + if (!Epoll.isAvailable()) { + throw new IllegalStateException("The epoll transport is not supported"); + } + } @Override public EpollSocketChannel newChannel() { return new EpollSocketChannel(); } + + @Override + public EpollEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new EpollEventLoopGroup(ioThreadsCount, threadFactory); + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java new file mode 100644 index 0000000000000000000000000000000000000000..97b822473964e029548435dfce14bc0f3f4f13db --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * A java.util.concurrent.Semaphore that always has Integer.Integer.MAX_VALUE free permits + * + * @author Alex Maltinsky + */ +public class InfiniteSemaphore extends Semaphore { + + public static final InfiniteSemaphore INSTANCE = new InfiniteSemaphore(); + private static final long serialVersionUID = 1L; + + private InfiniteSemaphore() { + super(Integer.MAX_VALUE); + } + + @Override + public void acquire() { + // NO-OP + } + + @Override + public void acquireUninterruptibly() { + // NO-OP + } + + @Override + public boolean tryAcquire() { + return true; + } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) { + return true; + } + + @Override + public void release() { + // NO-OP + } + + @Override + public void acquire(int permits) { + // NO-OP + } + + @Override + public void acquireUninterruptibly(int permits) { + // NO-OP + } + + @Override + public boolean tryAcquire(int permits) { + return true; + } + + @Override + public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { + return true; + } + + @Override + public void release(int permits) { + // NO-OP + } + + @Override + public int availablePermits() { + return Integer.MAX_VALUE; + } + + @Override + public int drainPermits() { + return Integer.MAX_VALUE; + } + + @Override + protected void reducePermits(int reduction) { + // NO-OP + } + + @Override + public boolean isFair() { + return true; + } + + @Override + protected Collection<Thread> getQueuedThreads() { + return Collections.emptyList(); + } +} + diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f54fe46157e30369c3e53a049278d997031d7c8b --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; + +import java.util.concurrent.ThreadFactory; + +class KQueueTransportFactory implements TransportFactory<KQueueSocketChannel, KQueueEventLoopGroup> { + + KQueueTransportFactory() { + try { + Class.forName("io.netty.channel.kqueue.KQueue"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The kqueue transport is not available"); + } + if (!KQueue.isAvailable()) { + throw new IllegalStateException("The kqueue transport is not supported"); + } + } + + @Override + public KQueueSocketChannel newChannel() { + return new KQueueSocketChannel(); + } + + @Override + public KQueueEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new KQueueEventLoopGroup(ioThreadsCount, threadFactory); + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java index 99bd6a4be42e441560a27b92704fd4988c434893..99c318afac194cd013a6c710bb2470f3ddfca409 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java @@ -16,6 +16,8 @@ package org.asynchttpclient.netty.channel; import org.asynchttpclient.exception.TooManyConnectionsException; import java.io.IOException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; @@ -23,21 +25,29 @@ import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; * Max connections limiter. * * @author Stepan Koltsov + * @author Alex Maltinsky */ public class MaxConnectionSemaphore implements ConnectionSemaphore { - private final NonBlockingSemaphoreLike freeChannels; - private final IOException tooManyConnections; + protected final Semaphore freeChannels; + protected final IOException tooManyConnections; + protected final int acquireTimeout; - MaxConnectionSemaphore(int maxConnections) { + MaxConnectionSemaphore(int maxConnections, int acquireTimeout) { tooManyConnections = unknownStackTrace(new TooManyConnectionsException(maxConnections), MaxConnectionSemaphore.class, "acquireChannelLock"); - freeChannels = maxConnections > 0 ? new NonBlockingSemaphore(maxConnections) : NonBlockingSemaphoreInfinite.INSTANCE; + freeChannels = maxConnections > 0 ? new Semaphore(maxConnections) : InfiniteSemaphore.INSTANCE; + this.acquireTimeout = Math.max(0, acquireTimeout); } @Override public void acquireChannelLock(Object partitionKey) throws IOException { - if (!freeChannels.tryAcquire()) - throw tooManyConnections; + try { + if (!freeChannels.tryAcquire(acquireTimeout, TimeUnit.MILLISECONDS)) { + throw tooManyConnections; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } @Override diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java index 76bd652a44dd286cf6eb98633ca7af6e86cc56ef..4a6f4dce20d1381398d4f787bc4a388a50de4eee 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java @@ -130,7 +130,7 @@ public final class NettyConnectListener<T> { @Override protected void onSuccess(Channel value) { try { - asyncHandler.onTlsHandshakeSuccess(); + asyncHandler.onTlsHandshakeSuccess(sslHandler.engine().getSession()); } catch (Exception e) { LOGGER.error("onTlsHandshakeSuccess crashed", e); NettyConnectListener.this.onFailure(channel, e); diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java similarity index 57% rename from client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java rename to client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java index 3d4fb91dbd46e4e314d9ea578db16a7761261e6b..d691ff270a15cf87291e79d46a363ce6cfd4ffd8 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -13,27 +13,22 @@ */ package org.asynchttpclient.netty.channel; -/** - * Non-blocking semaphore-like object with infinite permits. - * <p> - * So try-acquire always succeeds. - * - * @author Stepan Koltsov - */ -enum NonBlockingSemaphoreInfinite implements NonBlockingSemaphoreLike { - INSTANCE; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; - @Override - public void release() { - } +import java.util.concurrent.ThreadFactory; + +enum NioTransportFactory implements TransportFactory<NioSocketChannel, NioEventLoopGroup> { + + INSTANCE; @Override - public boolean tryAcquire() { - return true; + public NioSocketChannel newChannel() { + return new NioSocketChannel(); } @Override - public String toString() { - return NonBlockingSemaphore.class.getName(); + public NioEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new NioEventLoopGroup(ioThreadsCount, threadFactory); } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java deleted file mode 100644 index a7bd2eacfe936db6a47267ba59f2e378c448b31f..0000000000000000000000000000000000000000 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Semaphore-like API, but without blocking. - * - * @author Stepan Koltsov - */ -class NonBlockingSemaphore implements NonBlockingSemaphoreLike { - - private final AtomicInteger permits; - - NonBlockingSemaphore(int permits) { - this.permits = new AtomicInteger(permits); - } - - @Override - public void release() { - permits.incrementAndGet(); - } - - @Override - public boolean tryAcquire() { - for (; ; ) { - int count = permits.get(); - if (count <= 0) { - return false; - } - if (permits.compareAndSet(count, count - 1)) { - return true; - } - } - } - - @Override - public String toString() { - // mimic toString of Semaphore class - return super.toString() + "[Permits = " + permits + "]"; - } -} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java b/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java deleted file mode 100644 index 44303c9dfc799d815ba70dd078010d8390c91483..0000000000000000000000000000000000000000 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -/** - * Non-blocking semaphore API. - * - * @author Stepan Koltsov - */ -interface NonBlockingSemaphoreLike { - void release(); - - boolean tryAcquire(); -} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java index 5ebb348abf2e569bf7b6c49fe909ca8a6d9330d4..9ce1f20e937164c8470f5900ff1220e456ef1b1b 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java @@ -17,6 +17,8 @@ import org.asynchttpclient.exception.TooManyConnectionsPerHostException; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; @@ -25,37 +27,36 @@ import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; */ public class PerHostConnectionSemaphore implements ConnectionSemaphore { - private final ConnectionSemaphore globalSemaphore; + protected final ConcurrentHashMap<Object, Semaphore> freeChannelsPerHost = new ConcurrentHashMap<>(); + protected final int maxConnectionsPerHost; + protected final IOException tooManyConnectionsPerHost; + protected final int acquireTimeout; - private final ConcurrentHashMap<Object, NonBlockingSemaphore> freeChannelsPerHost = new ConcurrentHashMap<>(); - private final int maxConnectionsPerHost; - private final IOException tooManyConnectionsPerHost; - - PerHostConnectionSemaphore(int maxConnectionsPerHost, ConnectionSemaphore globalSemaphore) { - this.globalSemaphore = globalSemaphore; + PerHostConnectionSemaphore(int maxConnectionsPerHost, int acquireTimeout) { tooManyConnectionsPerHost = unknownStackTrace(new TooManyConnectionsPerHostException(maxConnectionsPerHost), PerHostConnectionSemaphore.class, "acquireChannelLock"); this.maxConnectionsPerHost = maxConnectionsPerHost; + this.acquireTimeout = Math.max(0, acquireTimeout); } @Override public void acquireChannelLock(Object partitionKey) throws IOException { - globalSemaphore.acquireChannelLock(partitionKey); - - if (!getFreeConnectionsForHost(partitionKey).tryAcquire()) { - globalSemaphore.releaseChannelLock(partitionKey); - throw tooManyConnectionsPerHost; + try { + if (!getFreeConnectionsForHost(partitionKey).tryAcquire(acquireTimeout, TimeUnit.MILLISECONDS)) { + throw tooManyConnectionsPerHost; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); } } @Override public void releaseChannelLock(Object partitionKey) { - globalSemaphore.releaseChannelLock(partitionKey); getFreeConnectionsForHost(partitionKey).release(); } - private NonBlockingSemaphoreLike getFreeConnectionsForHost(Object partitionKey) { + protected Semaphore getFreeConnectionsForHost(Object partitionKey) { return maxConnectionsPerHost > 0 ? - freeChannelsPerHost.computeIfAbsent(partitionKey, pk -> new NonBlockingSemaphore(maxConnectionsPerHost)) : - NonBlockingSemaphoreInfinite.INSTANCE; + freeChannelsPerHost.computeIfAbsent(partitionKey, pk -> new Semaphore(maxConnectionsPerHost)) : + InfiniteSemaphore.INSTANCE; } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java similarity index 67% rename from client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java rename to client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java index 907623bba6980da72fee1c8f080dcdf8c4bdb02d..76f45c2d288d7ad67cf0fe7e35f0343ff1a32065 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 AsyncHttpClient Project. All rights reserved. + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -13,15 +13,14 @@ */ package org.asynchttpclient.netty.channel; +import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; -import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.EventLoopGroup; -enum NioSocketChannelFactory implements ChannelFactory<NioSocketChannel> { +import java.util.concurrent.ThreadFactory; - INSTANCE; +public interface TransportFactory<C extends Channel, L extends EventLoopGroup> extends ChannelFactory<C> { + + L newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory); - @Override - public NioSocketChannel newChannel() { - return new NioSocketChannel(); - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java index ad29808d96d746e7f2194326df3f07f4d7eba683..a52f75fc8345b37df9d6f640ca0ddbf567d30824 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java @@ -30,6 +30,7 @@ import org.asynchttpclient.netty.channel.Channels; import org.asynchttpclient.netty.request.NettyRequestSender; import java.io.IOException; +import java.net.InetSocketAddress; @Sharable public final class HttpHandler extends AsyncHttpClientHandler { @@ -69,7 +70,7 @@ public final class HttpHandler extends AsyncHttpClientHandler { HttpRequest httpRequest = future.getNettyRequest().getHttpRequest(); logger.debug("\n\nRequest {}\n\nResponse {}\n", httpRequest, response); - future.setKeepAlive(config.getKeepAliveStrategy().keepAlive(future.getTargetRequest(), httpRequest, response)); + future.setKeepAlive(config.getKeepAliveStrategy().keepAlive((InetSocketAddress) channel.remoteAddress(), future.getTargetRequest(), httpRequest, response)); NettyResponseStatus status = new NettyResponseStatus(future.getUri(), response, channel); HttpHeaders responseHeaders = response.headers(); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java index 753df0020d30eaad0ddfd20edfeaa65b41c56582..eb2e98e36ff8824cfb7448f92ec1b93a9f9bac36 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java @@ -52,7 +52,7 @@ public class ConnectSuccessInterceptor { future.setReuseChannel(true); future.setConnectAllowed(false); - Request targetRequest = new RequestBuilder(future.getTargetRequest()).build(); + Request targetRequest = future.getTargetRequest().toBuilder().build(); if (whenHandshaked == null) { requestSender.drainChannelAndExecuteNextRequest(channel, future, targetRequest); } else { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index 02ee195622e926bac2123da55bf12142b3f67ce2..57436e9ae5e35757f77d21a0c801a8ada85ffd41 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -79,7 +79,7 @@ public class ProxyUnauthorized407Interceptor { // FIXME what's this??? future.setChannelState(ChannelState.NEW); - HttpHeaders requestHeaders = new DefaultHttpHeaders(false).add(request.getHeaders()); + HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); switch (proxyRealm.getScheme()) { case BASIC: @@ -163,7 +163,7 @@ public class ProxyUnauthorized407Interceptor { throw new IllegalStateException("Invalid Authentication scheme " + proxyRealm.getScheme()); } - RequestBuilder nextRequestBuilder = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders); + RequestBuilder nextRequestBuilder = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); if (future.getCurrentRequest().getUri().isSecured()) { nextRequestBuilder.setMethod(CONNECT); } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index 121bb71658dad96c9c5444b8d35505379f7596a6..d56b90fd24f1e7fe827b9d04d5e6514a7e19d140 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -111,6 +111,9 @@ public class Redirect30xInterceptor { requestBuilder.setBody(request.getByteBufferData()); else if (request.getBodyGenerator() != null) requestBuilder.setBody(request.getBodyGenerator()); + else if (isNonEmpty(request.getBodyParts())) { + requestBuilder.setBodyParts(request.getBodyParts()); + } } requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody)); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java index 30ba1bc3d6452656a62be61afd1b6529c54fa89b..269042529bb6c5ece2118af8dacb6b18eb247eac 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java @@ -77,7 +77,7 @@ public class Unauthorized401Interceptor { // FIXME what's this??? future.setChannelState(ChannelState.NEW); - HttpHeaders requestHeaders = new DefaultHttpHeaders(false).add(request.getHeaders()); + HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); switch (realm.getScheme()) { case BASIC: @@ -162,7 +162,7 @@ public class Unauthorized401Interceptor { throw new IllegalStateException("Invalid Authentication scheme " + realm.getScheme()); } - final Request nextRequest = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders).build(); + final Request nextRequest = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders).build(); LOGGER.debug("Sending authentication to {}", request.getUri()); if (future.isKeepAlive() diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 03255731f787c75fe08d5e8ffe0e203bf30d576d..32720acc10071e6ac258b13d6bf4d412cf80a2fe 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -437,8 +437,8 @@ public final class NettyRequestSender { } private void configureTransferAdapter(AsyncHandler<?> handler, HttpRequest httpRequest) { - HttpHeaders h = new DefaultHttpHeaders(false).set(httpRequest.headers()); - TransferCompletionHandler.class.cast(handler).headers(h); + HttpHeaders h = new DefaultHttpHeaders().set(httpRequest.headers()); + ((TransferCompletionHandler) handler).headers(h); } private void scheduleRequestTimeout(NettyResponseFuture<?> nettyResponseFuture, diff --git a/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java b/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java index ab38a66f945dafd7eb45dd912ed5f4b85a716d34..0a51e63e90919e34fe2b33a8f7f4c8429eee3d3e 100644 --- a/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java @@ -16,11 +16,13 @@ package org.asynchttpclient.netty.request; import io.netty.channel.Channel; import org.asynchttpclient.handler.ProgressAsyncHandler; import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelState; import org.asynchttpclient.netty.channel.Channels; import org.asynchttpclient.netty.future.StackTraceInspector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLException; import java.nio.channels.ClosedChannelException; public abstract class WriteListener { @@ -36,27 +38,27 @@ public abstract class WriteListener { this.notifyHeaders = notifyHeaders; } - private boolean abortOnThrowable(Channel channel, Throwable cause) { - if (cause != null) { - if (cause instanceof IllegalStateException || cause instanceof ClosedChannelException || StackTraceInspector.recoverOnReadOrWriteException(cause)) { - LOGGER.debug(cause.getMessage(), cause); - Channels.silentlyCloseChannel(channel); + private void abortOnThrowable(Channel channel, Throwable cause) { + if (future.getChannelState() == ChannelState.POOLED + && (cause instanceof IllegalStateException + || cause instanceof ClosedChannelException + || cause instanceof SSLException + || StackTraceInspector.recoverOnReadOrWriteException(cause))) { + LOGGER.debug("Write exception on pooled channel, letting retry trigger", cause); - } else { - future.abort(cause); - } - return true; + } else { + future.abort(cause); } - - return false; + Channels.silentlyCloseChannel(channel); } void operationComplete(Channel channel, Throwable cause) { future.touch(); - // The write operation failed. If the channel was cached, it means it got asynchronously closed. + // The write operation failed. If the channel was pooled, it means it got asynchronously closed. // Let's retry a second time. - if (abortOnThrowable(channel, cause)) { + if (cause != null) { + abortOnThrowable(channel, cause); return; } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java index 0d2535871369052d4f36dce5fc4507fe58e92556..9d4eacb165d5c517c6d7772f3c32f4a3f579859b 100644 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java @@ -23,6 +23,6 @@ public abstract class NettyDirectBody implements NettyBody { @Override public void write(Channel channel, NettyResponseFuture<?> future) { - throw new UnsupportedOperationException("This kind of body is supposed to be writen directly"); + throw new UnsupportedOperationException("This kind of body is supposed to be written directly"); } } diff --git a/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java b/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java index 60b14b56e55b0eec6937fd5c2db9818a0cf4478b..401c60a581ed8ab64e763d79485a9f5ccd63ed63 100644 --- a/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java @@ -19,6 +19,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.ReferenceCountUtil; import org.asynchttpclient.AsyncHttpClientConfig; import javax.net.ssl.SSLEngine; @@ -73,6 +74,11 @@ public class DefaultSslEngineFactory extends SslEngineFactoryBase { sslContext = buildSslContext(config); } + @Override + public void destroy() { + ReferenceCountUtil.release(sslContext); + } + /** * The last step of configuring the SslContextBuilder used to create an SslContext when no context is provided in the {@link AsyncHttpClientConfig}. This defaults to no-op and * is intended to be overridden as needed. diff --git a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java index e746adfdb55bbf1b814a18eeea9b2d43bd5ef901..034502785c9b92a8b3ff98129f3411afc7e34923 100755 --- a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java +++ b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java @@ -55,7 +55,7 @@ public abstract class TimeoutTimerTask implements TimerTask { void appendRemoteAddress(StringBuilder sb) { InetSocketAddress remoteAddress = timeoutsHolder.remoteAddress(); - sb.append(remoteAddress.getHostName()); + sb.append(remoteAddress.getHostString()); if (!remoteAddress.isUnresolved()) { sb.append('/').append(remoteAddress.getAddress().getHostAddress()); } diff --git a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java index 531eaadd894b47aaa7d37c94bbba6b59292b07f9..f6ab4ae2f3e4a3b2e010e1259000094aa041d6d0 100755 --- a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java +++ b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java @@ -155,7 +155,7 @@ public final class NettyWebSocket implements WebSocket { @Override public Future<Void> sendCloseFrame(int statusCode, String reasonText) { if (channel.isOpen()) { - return channel.writeAndFlush(new CloseWebSocketFrame(1000, "normal closure")); + return channel.writeAndFlush(new CloseWebSocketFrame(statusCode, reasonText)); } return ImmediateEventExecutor.INSTANCE.newSucceededFuture(null); } diff --git a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java index acb9fce5b1ccb3764e936449f698b55acc060947..aa92c5aaa1ac2dbc5e37b510200bd77ba977c2ba 100644 --- a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java +++ b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java @@ -199,7 +199,6 @@ public class OAuthSignatureCalculatorInstance { SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM); mac.init(signingKey); - mac.reset(); mac.update(message); return mac.doFinal(); } diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java index 3edf13ff1d80169ba2fede6a1d17c8d9cbacdda2..da42fcf6601a1b971c919337f88da31cf0340632 100644 --- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java +++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java @@ -35,7 +35,7 @@ public enum RequestHostnameResolver { public Future<List<InetSocketAddress>> resolve(NameResolver<InetAddress> nameResolver, InetSocketAddress unresolvedAddress, AsyncHandler<?> asyncHandler) { - final String hostname = unresolvedAddress.getHostName(); + final String hostname = unresolvedAddress.getHostString(); final int port = unresolvedAddress.getPort(); final Promise<List<InetSocketAddress>> promise = ImmediateEventExecutor.INSTANCE.newPromise(); diff --git a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java index 5a22abc361bf5a57a61a60d73605364de10598f2..11d00c05723c786602a8e05adbd5d6f20e0da4a6 100644 --- a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java @@ -156,7 +156,7 @@ public final class ProxyUtils { return null; } else { InetSocketAddress address = (InetSocketAddress) proxy.address(); - return proxyServer(address.getHostName(), address.getPort()).build(); + return proxyServer(address.getHostString(), address.getPort()).build(); } case DIRECT: return null; diff --git a/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java b/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java index a6df2fccf41a0f21deab1f6404056ecc5902db95..5a874af18069fdb022202bbfe335c8b2b98fb122 100644 --- a/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java +++ b/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java @@ -14,7 +14,6 @@ package org.asynchttpclient.webdav; import io.netty.handler.codec.http.HttpHeaders; -import org.asynchttpclient.AsyncCompletionHandlerBase; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.HttpResponseStatus; @@ -42,11 +41,24 @@ import java.util.List; * @param <T> the result type */ public abstract class WebDavCompletionHandlerBase<T> implements AsyncHandler<T> { - private final Logger logger = LoggerFactory.getLogger(AsyncCompletionHandlerBase.class); + private static final Logger LOGGER = LoggerFactory.getLogger(WebDavCompletionHandlerBase.class); + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY; private final List<HttpResponseBodyPart> bodyParts = Collections.synchronizedList(new ArrayList<>()); private HttpResponseStatus status; private HttpHeaders headers; + static { + DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + if (Boolean.getBoolean("org.asynchttpclient.webdav.enableDtd")) { + try { + DOCUMENT_BUILDER_FACTORY.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (ParserConfigurationException e) { + LOGGER.error("Failed to disable doctype declaration"); + throw new ExceptionInInitializerError(e); + } + } + } + /** * {@inheritDoc} */ @@ -75,13 +87,12 @@ public abstract class WebDavCompletionHandlerBase<T> implements AsyncHandler<T> } private Document readXMLResponse(InputStream stream) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document document; try { - document = factory.newDocumentBuilder().parse(stream); + document = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(stream); parse(document); } catch (SAXException | IOException | ParserConfigurationException e) { - logger.error(e.getMessage(), e); + LOGGER.error(e.getMessage(), e); throw new RuntimeException(e); } return document; @@ -94,7 +105,7 @@ public abstract class WebDavCompletionHandlerBase<T> implements AsyncHandler<T> Node node = statusNode.item(i); String value = node.getFirstChild().getNodeValue(); - int statusCode = Integer.valueOf(value.substring(value.indexOf(" "), value.lastIndexOf(" ")).trim()); + int statusCode = Integer.parseInt(value.substring(value.indexOf(" "), value.lastIndexOf(" ")).trim()); String statusText = value.substring(value.lastIndexOf(" ")); status = new HttpStatusWrapper(status, statusText, statusCode); } @@ -122,7 +133,7 @@ public abstract class WebDavCompletionHandlerBase<T> implements AsyncHandler<T> */ @Override public void onThrowable(Throwable t) { - logger.debug(t.getMessage(), t); + LOGGER.debug(t.getMessage(), t); } /** @@ -134,7 +145,7 @@ public abstract class WebDavCompletionHandlerBase<T> implements AsyncHandler<T> */ abstract public T onCompleted(WebDavResponse response) throws Exception; - private class HttpStatusWrapper extends HttpResponseStatus { + private static class HttpStatusWrapper extends HttpResponseStatus { private final HttpResponseStatus wrapped; diff --git a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties index cdc632f701a4172689593fa898a1d513042d64e6..cb846ac58052f0f0451795681e138942e1b09fc4 100644 --- a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties +++ b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties @@ -1,6 +1,7 @@ org.asynchttpclient.threadPoolName=AsyncHttpClient org.asynchttpclient.maxConnections=-1 org.asynchttpclient.maxConnectionsPerHost=-1 +org.asynchttpclient.acquireFreeChannelTimeout=0 org.asynchttpclient.connectTimeout=5000 org.asynchttpclient.pooledConnectionIdleTimeout=60000 org.asynchttpclient.connectionPoolCleanerPeriod=1000 @@ -31,6 +32,7 @@ org.asynchttpclient.sslSessionCacheSize=0 org.asynchttpclient.sslSessionTimeout=0 org.asynchttpclient.tcpNoDelay=true org.asynchttpclient.soReuseAddress=false +org.asynchttpclient.soKeepAlive=true org.asynchttpclient.soLinger=-1 org.asynchttpclient.soSndBuf=-1 org.asynchttpclient.soRcvBuf=-1 diff --git a/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java b/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java index e5ec9065765990f2ff1329e9657ca754f45b2d91..1547872aaa51a89397c0a520dc9e682ee5928c94 100644 --- a/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java +++ b/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java @@ -25,6 +25,8 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -40,7 +42,7 @@ import static org.testng.Assert.*; public class AsyncStreamHandlerTest extends HttpTest { - private static final String RESPONSE = "param_1_"; + private static final String RESPONSE = "param_1=value_1"; private static HttpServer server; @@ -93,18 +95,25 @@ public class AsyncStreamHandlerTest extends HttpTest { @Override public State onHeadersReceived(HttpHeaders headers) { assertContentTypesEquals(headers.get(CONTENT_TYPE), TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET); + for (Map.Entry<String, String> header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes(), US_ASCII)); return State.CONTINUE; } @Override public String onCompleted() { - return builder.toString().trim(); + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); } }).get(10, TimeUnit.SECONDS); @@ -174,17 +183,24 @@ public class AsyncStreamHandlerTest extends HttpTest { public State onHeadersReceived(HttpHeaders headers) { assertContentTypesEquals(headers.get(CONTENT_TYPE), TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET); onHeadersReceived.set(true); + for (Map.Entry<String, String> header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes())); return State.CONTINUE; } @Override public String onCompleted() { + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } return builder.toString().trim(); } @@ -254,17 +270,24 @@ public class AsyncStreamHandlerTest extends HttpTest { @Override public State onHeadersReceived(HttpHeaders headers) { responseHeaders.set(headers); + for (Map.Entry<String, String> header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes())); return State.CONTINUE; } @Override public String onCompleted() { + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } return builder.toString(); } }); diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java index 0a2631049144fb5ffb16c9465de47962d102e354..d38c930f91e4212f1755a093155aa7af020989d1 100755 --- a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java @@ -38,7 +38,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.ConnectException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.*; @@ -206,6 +209,40 @@ public class BasicHttpTest extends HttpTest { })); } + @Test + public void postChineseChar() throws Throwable { + withClient().run(client -> + withServer(server).run(server -> { + HttpHeaders h = new DefaultHttpHeaders(); + h.add(CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED); + + String chineseChar = "是"; + + Map<String, List<String>> m = new HashMap<>(); + m.put("param", Collections.singletonList(chineseChar)); + + Request request = post(getTargetUrl()).setHeaders(h).setFormParams(m).build(); + + server.enqueueEcho(); + + client.executeRequest(request, new AsyncCompletionHandlerAdapter() { + @Override + public Response onCompleted(Response response) { + assertEquals(response.getStatusCode(), 200); + String value; + try { + // headers must be encoded + value = URLDecoder.decode(response.getHeader("X-param"), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + assertEquals(value, chineseChar); + return response; + } + }).get(TIMEOUT, SECONDS); + })); + } + @Test public void headHasEmptyBody() throws Throwable { withClient().run(client -> diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java index 216e2604397a5c85f62acf40daa7dc55864f97d2..4395f0f49ba82a9f389fc2c753b45fdef0eb80a3 100644 --- a/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java @@ -107,7 +107,7 @@ public class BasicHttpsTest extends HttpTest { public void multipleConcurrentPostRequestsOverHttpsWithDisabledKeepAliveStrategy() throws Throwable { logger.debug(">>> multipleConcurrentPostRequestsOverHttpsWithDisabledKeepAliveStrategy"); - KeepAliveStrategy keepAliveStrategy = (ahcRequest, nettyRequest, nettyResponse) -> !ahcRequest.getUri().isSecured(); + KeepAliveStrategy keepAliveStrategy = (remoteAddress, ahcRequest, nettyRequest, nettyResponse) -> !ahcRequest.getUri().isSecured(); withClient(config().setSslEngineFactory(createSslEngineFactory()).setKeepAliveStrategy(keepAliveStrategy)).run(client -> withServer(server).run(server -> { diff --git a/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java index 41fed53a4c3774f350397bef31ab9a23ed3de066..968c408fbcbdcc07d5469b9934e2bc927f721462 100644 --- a/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java +++ b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java @@ -72,7 +72,7 @@ public class RequestBuilderTest { public void testChaining() { Request request = get("http://foo.com").addQueryParam("x", "value").build(); - Request request2 = new RequestBuilder(request).build(); + Request request2 = request.toBuilder().build(); assertEquals(request2.getUri(), request.getUri()); } diff --git a/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java b/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java index 5992bf3ed59b3fbe60d72a93a9be1aa2b4b3a52f..fcf34896f6f3e5bafc8da751ac724a6ec7ddef98 100644 --- a/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java +++ b/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java @@ -33,7 +33,7 @@ public class MaxTotalConnectionTest extends AbstractBasicTest { @Test(groups = "online") public void testMaxTotalConnectionsExceedingException() throws IOException { - String[] urls = new String[]{"http://google.com", "http://github.com/"}; + String[] urls = new String[]{"https://google.com", "https://github.com"}; AsyncHttpClientConfig config = config() .setConnectTimeout(1000) @@ -69,7 +69,7 @@ public class MaxTotalConnectionTest extends AbstractBasicTest { @Test(groups = "online") public void testMaxTotalConnections() throws Exception { - String[] urls = new String[]{"http://google.com", "http://gatling.io"}; + String[] urls = new String[]{"https://google.com", "https://github.com"}; final CountDownLatch latch = new CountDownLatch(2); final AtomicReference<Throwable> ex = new AtomicReference<>(); diff --git a/client/src/test/java/org/asynchttpclient/filter/FilterTest.java b/client/src/test/java/org/asynchttpclient/filter/FilterTest.java index 10b36507a527cffeadc3df2e56af001ab2a9a428..14997d6234f8184cf947e0588b8c41966fa67822 100644 --- a/client/src/test/java/org/asynchttpclient/filter/FilterTest.java +++ b/client/src/test/java/org/asynchttpclient/filter/FilterTest.java @@ -101,7 +101,7 @@ public class FilterTest extends AbstractBasicTest { ResponseFilter responseFilter = new ResponseFilter() { public <T> FilterContext<T> filter(FilterContext<T> ctx) { if (replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("X-Replay", "true").build(); + Request request = ctx.getRequest().toBuilder().addHeader("X-Replay", "true").build(); return new FilterContext.FilterContextBuilder<T>().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; @@ -123,7 +123,7 @@ public class FilterTest extends AbstractBasicTest { ResponseFilter responseFilter = new ResponseFilter() { public <T> FilterContext<T> filter(FilterContext<T> ctx) { if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 200 && replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("X-Replay", "true").build(); + Request request = ctx.getRequest().toBuilder().addHeader("X-Replay", "true").build(); return new FilterContext.FilterContextBuilder<T>().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; @@ -145,7 +145,7 @@ public class FilterTest extends AbstractBasicTest { ResponseFilter responseFilter = new ResponseFilter() { public <T> FilterContext<T> filter(FilterContext<T> ctx) { if (ctx.getResponseHeaders() != null && ctx.getResponseHeaders().get("Ping").equals("Pong") && replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("Ping", "Pong").build(); + Request request = ctx.getRequest().toBuilder().addHeader("Ping", "Pong").build(); return new FilterContext.FilterContextBuilder<T>().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java deleted file mode 100644 index a387ba408bf0ef56e0bb42d72665bfdc17e24f62..0000000000000000000000000000000000000000 --- a/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -import org.testng.annotations.Test; - -import java.util.concurrent.Semaphore; - -import static org.testng.Assert.*; - -/** - * @author Stepan Koltsov - */ -public class NonBlockingSemaphoreTest { - - @Test - public void test0() { - Mirror mirror = new Mirror(0); - assertFalse(mirror.tryAcquire()); - } - - @Test - public void three() { - Mirror mirror = new Mirror(3); - for (int i = 0; i < 3; ++i) { - assertTrue(mirror.tryAcquire()); - } - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertTrue(mirror.tryAcquire()); - } - - @Test - public void negative() { - Mirror mirror = new Mirror(-1); - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertTrue(mirror.tryAcquire()); - } - - private static class Mirror { - private final Semaphore real; - private final NonBlockingSemaphore nonBlocking; - - Mirror(int permits) { - real = new Semaphore(permits); - nonBlocking = new NonBlockingSemaphore(permits); - } - - boolean tryAcquire() { - boolean a = real.tryAcquire(); - boolean b = nonBlocking.tryAcquire(); - assertEquals(a, b); - return a; - } - - void release() { - real.release(); - nonBlocking.release(); - } - } - -} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..7bff799ceb2a08b8caa6ac1e6fc694c7f6fdde24 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java @@ -0,0 +1,52 @@ +package org.asynchttpclient.netty.channel; + +class SemaphoreRunner { + + final ConnectionSemaphore semaphore; + final Thread acquireThread; + + volatile long acquireTime; + volatile Exception acquireException; + + public SemaphoreRunner(ConnectionSemaphore semaphore, Object partitionKey) { + this.semaphore = semaphore; + this.acquireThread = new Thread(() -> { + long beforeAcquire = System.currentTimeMillis(); + try { + semaphore.acquireChannelLock(partitionKey); + } catch (Exception e) { + acquireException = e; + } finally { + acquireTime = System.currentTimeMillis() - beforeAcquire; + } + }); + } + + public void acquire() { + this.acquireThread.start(); + } + + public void interrupt() { + this.acquireThread.interrupt(); + } + + public void await() { + try { + this.acquireThread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public boolean finished() { + return !this.acquireThread.isAlive(); + } + + public long getAcquireTime() { + return acquireTime; + } + + public Exception getAcquireException() { + return acquireException; + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java new file mode 100644 index 0000000000000000000000000000000000000000..125cd9b066bf6f6c44fde9590920ad33e261ff7e --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java @@ -0,0 +1,143 @@ +package org.asynchttpclient.netty.channel; + +import org.asynchttpclient.exception.TooManyConnectionsException; +import org.asynchttpclient.exception.TooManyConnectionsPerHostException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.testng.AssertJUnit.*; + +public class SemaphoreTest { + + static final int CHECK_ACQUIRE_TIME__PERMITS = 10; + static final int CHECK_ACQUIRE_TIME__TIMEOUT = 100; + + static final int NON_DETERMINISTIC__INVOCATION_COUNT = 10; + static final int NON_DETERMINISTIC__SUCCESS_PERCENT = 70; + + private final Object PK = new Object(); + + @DataProvider(name = "permitsAndRunnersCount") + public Object[][] permitsAndRunnersCount() { + Object[][] objects = new Object[100][]; + int row = 0; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + objects[row++] = new Object[]{i, j}; + } + } + return objects; + } + + @Test(timeOut = 1000, dataProvider = "permitsAndRunnersCount") + public void maxConnectionCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new MaxConnectionSemaphore(permitCount, 0), permitCount, runnerCount); + } + + @Test(timeOut = 1000, dataProvider = "permitsAndRunnersCount") + public void perHostCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new PerHostConnectionSemaphore(permitCount, 0), permitCount, runnerCount); + } + + @Test(timeOut = 3000, dataProvider = "permitsAndRunnersCount") + public void combinedCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(permitCount, permitCount, 0), permitCount, runnerCount); + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(0, permitCount, 0), permitCount, runnerCount); + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(permitCount, 0, 0), permitCount, runnerCount); + } + + private void allSemaphoresCheckPermitCount(ConnectionSemaphore semaphore, int permitCount, int runnerCount) { + List<SemaphoreRunner> runners = IntStream.range(0, runnerCount) + .mapToObj(i -> new SemaphoreRunner(semaphore, PK)) + .collect(Collectors.toList()); + runners.forEach(SemaphoreRunner::acquire); + runners.forEach(SemaphoreRunner::await); + + long tooManyConnectionsCount = runners.stream().map(SemaphoreRunner::getAcquireException) + .filter(Objects::nonNull) + .filter(e -> e instanceof IOException) + .count(); + + long acquired = runners.stream().map(SemaphoreRunner::getAcquireException) + .filter(Objects::isNull) + .count(); + + int expectedAcquired = permitCount > 0 ? Math.min(permitCount, runnerCount) : runnerCount; + + assertEquals(expectedAcquired, acquired); + assertEquals(runnerCount - acquired, tooManyConnectionsCount); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void maxConnectionCheckAcquireTime() { + checkAcquireTime(new MaxConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void perHostCheckAcquireTime() { + checkAcquireTime(new PerHostConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void combinedCheckAcquireTime() { + checkAcquireTime(new CombinedConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, + CHECK_ACQUIRE_TIME__PERMITS, + CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + private void checkAcquireTime(ConnectionSemaphore semaphore) { + List<SemaphoreRunner> runners = IntStream.range(0, CHECK_ACQUIRE_TIME__PERMITS * 2) + .mapToObj(i -> new SemaphoreRunner(semaphore, PK)) + .collect(Collectors.toList()); + long acquireStartTime = System.currentTimeMillis(); + runners.forEach(SemaphoreRunner::acquire); + runners.forEach(SemaphoreRunner::await); + long timeToAcquire = System.currentTimeMillis() - acquireStartTime; + + assertTrue("Semaphore acquired too soon: " + timeToAcquire+" ms",timeToAcquire >= (CHECK_ACQUIRE_TIME__TIMEOUT - 50)); //Lower Bound + assertTrue("Semaphore acquired too late: " + timeToAcquire+" ms",timeToAcquire <= (CHECK_ACQUIRE_TIME__TIMEOUT + 300)); //Upper Bound + } + + @Test(timeOut = 1000) + public void maxConnectionCheckRelease() throws IOException { + checkRelease(new MaxConnectionSemaphore(1, 0)); + } + + @Test(timeOut = 1000) + public void perHostCheckRelease() throws IOException { + checkRelease(new PerHostConnectionSemaphore(1, 0)); + } + + @Test(timeOut = 1000) + public void combinedCheckRelease() throws IOException { + checkRelease(new CombinedConnectionSemaphore(1, 1, 0)); + } + + private void checkRelease(ConnectionSemaphore semaphore) throws IOException { + semaphore.acquireChannelLock(PK); + boolean tooManyCaught = false; + try { + semaphore.acquireChannelLock(PK); + } catch (TooManyConnectionsException | TooManyConnectionsPerHostException e) { + tooManyCaught = true; + } + assertTrue(tooManyCaught); + tooManyCaught = false; + semaphore.releaseChannelLock(PK); + try { + semaphore.acquireChannelLock(PK); + } catch (TooManyConnectionsException | TooManyConnectionsPerHostException e) { + tooManyCaught = true; + } + assertFalse(tooManyCaught); + } + + +} + diff --git a/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java b/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java index 252de41913355de8dd3d5eed5f71e5854af97fe7..8047c5f843a89848adb409896df4e4493c5f5b01 100644 --- a/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java +++ b/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java @@ -20,6 +20,7 @@ import org.asynchttpclient.Response; import org.asynchttpclient.netty.request.NettyRequest; import org.testng.Assert; +import javax.net.ssl.SSLSession; import java.net.InetSocketAddress; import java.util.List; import java.util.Queue; @@ -128,7 +129,8 @@ public class EventCollectingHandler extends AsyncCompletionHandlerBase { } @Override - public void onTlsHandshakeSuccess() { + public void onTlsHandshakeSuccess(SSLSession sslSession) { + Assert.assertNotNull(sslSession); firedEvents.add(TLS_HANDSHAKE_SUCCESS_EVENT); } diff --git a/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java b/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java index ae387469a7955c5a39c388bc7a43c27464d96c0f..9b74656b5eebbe196fed29ae415f4adbd65d30db 100644 --- a/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java +++ b/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java @@ -25,6 +25,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Closeable; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.Map.Entry; import java.util.concurrent.ConcurrentLinkedQueue; @@ -208,8 +210,9 @@ public class HttpServer implements Closeable { response.addHeader("X-" + headerName, request.getHeader(headerName)); } + StringBuilder requestBody = new StringBuilder(); for (Entry<String, String[]> e : baseRequest.getParameterMap().entrySet()) { - response.addHeader("X-" + e.getKey(), e.getValue()[0]); + response.addHeader("X-" + e.getKey(), URLEncoder.encode(e.getValue()[0], StandardCharsets.UTF_8.name())); } Cookie[] cs = request.getCookies(); @@ -219,14 +222,6 @@ public class HttpServer implements Closeable { } } - Enumeration<String> parameterNames = request.getParameterNames(); - StringBuilder requestBody = new StringBuilder(); - while (parameterNames.hasMoreElements()) { - String param = parameterNames.nextElement(); - response.addHeader("X-" + param, request.getParameter(param)); - requestBody.append(param); - requestBody.append("_"); - } if (requestBody.length() > 0) { response.getOutputStream().write(requestBody.toString().getBytes()); } diff --git a/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java b/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java index 52aaefc3a575c0cdf214d25209d3377301142949..ebbfb511fa24d57edadc1936a925241353078c91 100644 --- a/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java @@ -51,7 +51,8 @@ public class CloseCodeReasonMessageTest extends AbstractBasicWebSocketTest { c.prepareGet(getTargetUrl()).execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new Listener(latch, text)).build()).get(); latch.await(); - assertEquals(text.get(), "1001-Idle Timeout"); + // used to be correct 001-Idle Timeout prior to Jetty 9.4.15... + assertEquals(text.get(), "1000-"); } } diff --git a/debian/changelog b/debian/changelog index 98524b5ce8264a7177c0ed42b947298bce49a407..fbd5e82e25bc15e07ff8359afbf729a363812002 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +async-http-client (2.11.0-1) unstable; urgency=medium + + * Team upload. + * Update to upstream v2.11.0 + * Use debhelper-compat. + - Update to compat level 12. + * Update Standards-Version to 4.5.0 + * Add Rules-Requires-Root: no. + * Use secure copyright format uri. + + -- Sudip Mukherjee <sudipm.mukherjee@gmail.com> Tue, 14 Apr 2020 23:21:02 +0100 + async-http-client (2.6.0-1) unstable; urgency=medium * New upstream release diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de3947675361a7770d29b8982c407b0ec6b2a0..0000000000000000000000000000000000000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/control b/debian/control index bca2d20a8f026ee709d42f95cf04b907c899545f..cfc21c30d5b5226a6ecd2b2fc12cf07154ab3fce 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Debian Java Maintainers <pkg-java-maintainers@lists.alioth.debian.org> Uploaders: Emmanuel Bourg <ebourg@apache.org> Build-Depends: - debhelper (>= 11~), + debhelper-compat (= 12), default-jdk, libactivation-java, libmaven-bundle-plugin-java, @@ -12,8 +12,10 @@ Build-Depends: libnetty-reactive-streams-java, libreactive-streams-java, libslf4j-java, - maven-debian-helper (>= 2.2) -Standards-Version: 4.2.1 + maven-debian-helper (>= 2.2), + libhamcrest-java +Standards-Version: 4.5.0 +Rules-Requires-Root: no Vcs-Git: https://salsa.debian.org/java-team/async-http-client.git Vcs-Browser: https://salsa.debian.org/java-team/async-http-client Homepage: https://github.com/AsyncHttpClient/async-http-client diff --git a/debian/copyright b/debian/copyright index 9dc11d826445eb7198f763bfc940ab75fe737e1a..a9ada339525f5ae6493f547c251f31d7228dd925 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0 Upstream-Name: Asynchronous Http Client Source: https://github.com/AsyncHttpClient/async-http-client diff --git a/debian/libasync-http-client-java.poms b/debian/libasync-http-client-java.poms index fea76435f470ff42a0b5a5c03cc8f3b52c83deb0..9bd0abbe9990f578576997e279d75cd5dbe0a4d3 100644 --- a/debian/libasync-http-client-java.poms +++ b/debian/libasync-http-client-java.poms @@ -28,6 +28,7 @@ pom.xml --no-parent --has-package-version client/pom.xml --has-package-version example/pom.xml --ignore +bom/pom.xml --ignore extras/pom.xml --ignore extras/guava/pom.xml --ignore extras/jdeferred/pom.xml --ignore diff --git a/debian/maven.rules b/debian/maven.rules index 001e4d9ffae8cdffeba1cc2ce66ec65cb97c2134..de361487cdd90016750f3159449678e0a38f9cbf 100644 --- a/debian/maven.rules +++ b/debian/maven.rules @@ -19,3 +19,5 @@ com.ning async-http-client jar s/.*/debian/ * * s/org.jboss.netty/io.netty/ netty * s/.*/debian/ * * io.netty netty-transport-native-epoll jar s/.*/debian/ s/linux-x86_64// * +org.hamcrest s/hamcrest/hamcrest-all/ * s/.*/debian/ * * +io.netty netty-transport-native-kqueue * s/.*/debian/ * * diff --git a/debian/patches/remove_classifier.patch b/debian/patches/remove_classifier.patch new file mode 100644 index 0000000000000000000000000000000000000000..88434652c94cd76a81a890bd15e1abce63007fac --- /dev/null +++ b/debian/patches/remove_classifier.patch @@ -0,0 +1,26 @@ +Description: Remove classifier + +Author: Sudip Mukherjee <sudipm.mukherjee@gmail.com> + +--- + +--- async-http-client-2.11.0.orig/client/pom.xml ++++ async-http-client-2.11.0/client/pom.xml +@@ -58,7 +58,6 @@ + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport-native-kqueue</artifactId> +- <classifier>osx-x86_64</classifier> + </dependency> + <dependency> + <groupId>org.reactivestreams</groupId> +--- async-http-client-2.11.0.orig/pom.xml ++++ async-http-client-2.11.0/pom.xml +@@ -308,7 +308,6 @@ + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport-native-kqueue</artifactId> +- <classifier>osx-x86_64</classifier> + <version>${netty.version}</version> + <optional>true</optional> + </dependency> diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000000000000000000000000000000000000..694d7963c579ee2a38a9c85d6d27c336704bf64e --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +remove_classifier.patch diff --git a/example/pom.xml b/example/pom.xml index b137d82b484891517ddcc467a555f2721fd7bc7c..38d3d740c4d23cf2e0cf70a108b2f6eb21ad8a30 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-project</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client-example</artifactId> @@ -11,6 +11,11 @@ <description> The Async Http Client example. </description> + + <properties> + <javaModuleName>org.asynchttpclient.example</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>org.asynchttpclient</groupId> diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 2097f364a18383818db49c6c68bdb1b0885039bd..ac338ae020db524f0afe4e7ba4622387f6c10c6b 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-extras-parent</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client-extras-guava</artifactId> @@ -11,11 +11,15 @@ The Async Http Client Guava Extras. </description> + <properties> + <javaModuleName>org.asynchttpclient.extras.guava</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> - <version>14.0.1</version> + <version>28.2-jre</version> </dependency> </dependencies> </project> \ No newline at end of file diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index 6dba93bd4d358980a62bd6093343e03c90d0e834..12ee456acc547ca3a888e46e2e328f8276914cd8 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,11 +18,16 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-jdeferred</artifactId> <name>Asynchronous Http Client JDeferred Extras</name> <description>The Async Http Client jDeffered Extras.</description> + + <properties> + <javaModuleName>org.asynchttpclient.extras.jdeferred</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>org.jdeferred</groupId> diff --git a/extras/pom.xml b/extras/pom.xml index ffa65b3ed19d686cfbd3ad0b70fdd4353e733244..caecaa8acab02a91d7b11b5bed9eb3a900cc8ed1 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-project</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client-extras-parent</artifactId> diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index ee3a66193fb51846156c20f6409f748922b70926..a0eb2145a8df22ece2d42843d27b162c26cac1f0 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-extras-parent</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client-extras-registry</artifactId> @@ -10,4 +10,9 @@ <description> The Async Http Client Registry Extras. </description> + + <properties> + <javaModuleName>org.asynchttpclient.extras.registry</javaModuleName> + </properties> + </project> \ No newline at end of file diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 9c1da9b04ef5bc47500f05d231eacd56661eb2c6..70af053f24a0df6ca5b8382c050baaa01b8ac80c 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-retrofit2</artifactId> @@ -12,8 +12,9 @@ <description>The Async Http Client Retrofit2 Extras.</description> <properties> - <retrofit2.version>2.4.0</retrofit2.version> - <lombok.version>1.16.20</lombok.version> + <retrofit2.version>2.7.2</retrofit2.version> + <lombok.version>1.18.12</lombok.version> + <javaModuleName>org.asynchttpclient.extras.retrofit2</javaModuleName> </properties> <dependencies> diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java index b9517f9fb1347564b5a662a63b06fd72c7c0a826..d5534a9ce1a96c3bb181556590044016531c9e59 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -17,6 +17,7 @@ import lombok.*; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import okio.Buffer; +import okio.Timeout; import org.asynchttpclient.AsyncCompletionHandler; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.RequestBuilder; @@ -29,6 +30,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Supplier; /** * {@link AsyncHttpClient} <a href="http://square.github.io/retrofit/">Retrofit2</a> {@link okhttp3.Call} @@ -37,16 +39,9 @@ import java.util.function.Consumer; @Value @Builder(toBuilder = true) @Slf4j -class AsyncHttpClientCall implements Cloneable, okhttp3.Call { - /** - * Default {@link #execute()} timeout in milliseconds (value: <b>{@value}</b>) - * - * @see #execute() - * @see #executeTimeoutMillis - */ - public static final long DEFAULT_EXECUTE_TIMEOUT_MILLIS = 30_000; - +public class AsyncHttpClientCall implements Cloneable, okhttp3.Call { private static final ResponseBody EMPTY_BODY = ResponseBody.create(null, ""); + /** * Tells whether call has been executed. * @@ -54,37 +49,38 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { * @see #isCanceled() */ private final AtomicReference<CompletableFuture<Response>> futureRef = new AtomicReference<>(); + /** - * HttpClient instance. + * {@link AsyncHttpClient} supplier */ @NonNull - AsyncHttpClient httpClient; - /** - * {@link #execute()} response timeout in milliseconds. - */ - @Builder.Default - long executeTimeoutMillis = DEFAULT_EXECUTE_TIMEOUT_MILLIS; + Supplier<AsyncHttpClient> httpClientSupplier; + /** * Retrofit request. */ @NonNull @Getter(AccessLevel.NONE) Request request; + /** * List of consumers that get called just before actual async-http-client request is being built. */ @Singular("requestCustomizer") List<Consumer<RequestBuilder>> requestCustomizers; + /** * List of consumers that get called just before actual HTTP request is being fired. */ @Singular("onRequestStart") List<Consumer<Request>> onRequestStart; + /** * List of consumers that get called when HTTP request finishes with an exception. */ @Singular("onRequestFailure") List<Consumer<Throwable>> onRequestFailure; + /** * List of consumers that get called when HTTP request finishes successfully. */ @@ -130,7 +126,7 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { @Override public Response execute() throws IOException { try { - return executeHttpRequest().get(getExecuteTimeoutMillis(), TimeUnit.MILLISECONDS); + return executeHttpRequest().get(getRequestTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (ExecutionException e) { throw toIOException(e.getCause()); } catch (Exception e) { @@ -148,7 +144,7 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { @Override public void cancel() { val future = futureRef.get(); - if (future != null) { + if (future != null && !future.isDone()) { if (!future.cancel(true)) { log.warn("Cannot cancel future: {}", future); } @@ -167,6 +163,20 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { return future != null && future.isCancelled(); } + @Override + public Timeout timeout() { + return new Timeout().timeout(getRequestTimeoutMillis(), TimeUnit.MILLISECONDS); + } + + /** + * Returns HTTP request timeout in milliseconds, retrieved from http client configuration. + * + * @return request timeout in milliseconds. + */ + protected long getRequestTimeoutMillis() { + return Math.abs(getHttpClient().getConfig().getRequestTimeout()); + } + @Override public Call clone() { return toBuilder().build(); @@ -230,6 +240,20 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { return future; } + /** + * Returns HTTP client. + * + * @return http client + * @throws IllegalArgumentException if {@link #httpClientSupplier} returned {@code null}. + */ + protected AsyncHttpClient getHttpClient() { + val httpClient = httpClientSupplier.get(); + if (httpClient == null) { + throw new IllegalStateException("Async HTTP client instance supplier " + httpClientSupplier + " returned null."); + } + return httpClient; + } + /** * Converts async-http-client response to okhttp response. * diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java index b7c087fd350c2ef86868213e1ad015ca031cf789..0077cd32e3903c9ef09b8eef698320575801f5b6 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java @@ -19,31 +19,35 @@ import org.asynchttpclient.AsyncHttpClient; import java.util.List; import java.util.function.Consumer; +import java.util.function.Supplier; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers; /** - * {@link AsyncHttpClient} implementation of Retrofit2 {@link Call.Factory} + * {@link AsyncHttpClient} implementation of <a href="http://square.github.io/retrofit/">Retrofit2</a> + * {@link Call.Factory}. */ @Value @Builder(toBuilder = true) public class AsyncHttpClientCallFactory implements Call.Factory { /** - * {@link AsyncHttpClient} in use. + * Supplier of {@link AsyncHttpClient}. */ @NonNull - AsyncHttpClient httpClient; + @Getter(AccessLevel.NONE) + Supplier<AsyncHttpClient> httpClientSupplier; /** * List of {@link Call} builder customizers that are invoked just before creating it. */ @Singular("callCustomizer") + @Getter(AccessLevel.PACKAGE) List<Consumer<AsyncHttpClientCall.AsyncHttpClientCallBuilder>> callCustomizers; @Override public Call newCall(Request request) { val callBuilder = AsyncHttpClientCall.builder() - .httpClient(httpClient) + .httpClientSupplier(httpClientSupplier) .request(request); // customize builder before creating a call @@ -52,4 +56,35 @@ public class AsyncHttpClientCallFactory implements Call.Factory { // create a call return callBuilder.build(); } -} + + /** + * Returns {@link AsyncHttpClient} from {@link #httpClientSupplier}. + * + * @return http client. + */ + AsyncHttpClient getHttpClient() { + return httpClientSupplier.get(); + } + + /** + * Builder for {@link AsyncHttpClientCallFactory}. + */ + public static class AsyncHttpClientCallFactoryBuilder { + /** + * {@link AsyncHttpClient} supplier that returns http client to be used to execute HTTP requests. + */ + private Supplier<AsyncHttpClient> httpClientSupplier; + + /** + * Sets concrete http client to be used by the factory to execute HTTP requests. Invocation of this method + * overrides any previous http client supplier set by {@link #httpClientSupplier(Supplier)}! + * + * @param httpClient http client + * @return reference to itself. + * @see #httpClientSupplier(Supplier) + */ + public AsyncHttpClientCallFactoryBuilder httpClient(@NonNull AsyncHttpClient httpClient) { + return httpClientSupplier(() -> httpClient); + } + } +} \ No newline at end of file diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java index 730ab5d007bd85230525ad54c6dc6304aeaa9a58..4b7605a8134f71ba6e017833d62f04247aab1c7c 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -14,23 +14,34 @@ package org.asynchttpclient.extras.retrofit; import lombok.extern.slf4j.Slf4j; import lombok.val; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.RequestBuilder; import org.testng.annotations.Test; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.REQUEST; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.createConsumer; import static org.mockito.Mockito.mock; import static org.testng.Assert.*; @Slf4j public class AsyncHttpClientCallFactoryTest { + private static final MediaType MEDIA_TYPE = MediaType.parse("application/json"); + private static final String JSON_BODY = "{\"foo\": \"bar\"}"; + private static final RequestBody BODY = RequestBody.create(MEDIA_TYPE, JSON_BODY); + private static final String URL = "http://localhost:11000/foo/bar?a=b&c=d"; + private static final Request REQUEST = new Request.Builder() + .post(BODY) + .addHeader("X-Foo", "Bar") + .url(URL) + .build(); @Test void newCallShouldProduceExpectedResult() { // given @@ -109,12 +120,12 @@ public class AsyncHttpClientCallFactoryTest { }; Consumer<AsyncHttpClientCall.AsyncHttpClientCallBuilder> callCustomizer = callBuilder -> - callBuilder - .requestCustomizer(requestCustomizer) - .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb)) - .onRequestSuccess(createConsumer(numRequestSuccess)) - .onRequestFailure(createConsumer(numRequestFailure)) - .onRequestStart(createConsumer(numRequestStart)); + callBuilder + .requestCustomizer(requestCustomizer) + .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb)) + .onRequestSuccess(createConsumer(numRequestSuccess)) + .onRequestFailure(createConsumer(numRequestFailure)) + .onRequestStart(createConsumer(numRequestStart)); // create factory val factory = AsyncHttpClientCallFactory.builder() @@ -151,4 +162,65 @@ public class AsyncHttpClientCallFactoryTest { assertNotNull(call.getRequestCustomizers()); assertTrue(call.getRequestCustomizers().size() == 2); } + + @Test(expectedExceptions = NullPointerException.class, + expectedExceptionsMessageRegExp = "httpClientSupplier is marked non-null but is null") + void shouldThrowISEIfHttpClientIsNotDefined() { + // given + val factory = AsyncHttpClientCallFactory.builder() + .build(); + + // when + val httpClient = factory.getHttpClient(); + + // then + assertNull(httpClient); + } + + @Test + void shouldUseHttpClientInstanceIfSupplierIsNotAvailable() { + // given + val httpClient = mock(AsyncHttpClient.class); + + val factory = AsyncHttpClientCallFactory.builder() + .httpClient(httpClient) + .build(); + + // when + val usedHttpClient = factory.getHttpClient(); + + // then + assertTrue(usedHttpClient == httpClient); + + // when + val call = (AsyncHttpClientCall) factory.newCall(REQUEST); + + // then: call should contain correct http client + assertTrue(call.getHttpClient()== httpClient); + } + + @Test + void shouldPreferHttpClientSupplierOverHttpClient() { + // given + val httpClientA = mock(AsyncHttpClient.class); + val httpClientB = mock(AsyncHttpClient.class); + + val factory = AsyncHttpClientCallFactory.builder() + .httpClient(httpClientA) + .httpClientSupplier(() -> httpClientB) + .build(); + + // when + val usedHttpClient = factory.getHttpClient(); + + // then + assertTrue(usedHttpClient == httpClientB); + + // when: try to create new call + val call = (AsyncHttpClientCall) factory.newCall(REQUEST); + + // then: call should contain correct http client + assertNotNull(call); + assertTrue(call.getHttpClient() == httpClientB); + } } diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java index 84ed4ddf470622fa142d938dd1b61e21dddb4371..e655ed73fc5a1ec28a922b6a74a715874b230464 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java @@ -14,16 +14,18 @@ package org.asynchttpclient.extras.retrofit; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.EmptyHttpHeaders; -import lombok.val; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; import org.asynchttpclient.AsyncCompletionHandler; import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.BoundRequestBuilder; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; import org.asynchttpclient.Response; import org.mockito.ArgumentCaptor; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -35,17 +37,32 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Supplier; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumer; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; +@Slf4j public class AsyncHttpClientCallTest { static final Request REQUEST = new Request.Builder().url("http://www.google.com/").build(); + static final DefaultAsyncHttpClientConfig DEFAULT_AHC_CONFIG = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(1_000) + .build(); + + private AsyncHttpClient httpClient; + private Supplier<AsyncHttpClient> httpClientSupplier = () -> httpClient; + + @BeforeMethod + void setup() { + httpClient = mock(AsyncHttpClient.class); + when(httpClient.getConfig()).thenReturn(DEFAULT_AHC_CONFIG); + } @Test(expectedExceptions = NullPointerException.class, dataProvider = "first") void builderShouldThrowInCaseOfMissingProperties(AsyncHttpClientCall.AsyncHttpClientCallBuilder builder) { @@ -54,12 +71,10 @@ public class AsyncHttpClientCallTest { @DataProvider(name = "first") Object[][] dataProviderFirst() { - val httpClient = mock(AsyncHttpClient.class); - return new Object[][]{ {AsyncHttpClientCall.builder()}, {AsyncHttpClientCall.builder().request(REQUEST)}, - {AsyncHttpClientCall.builder().httpClient(httpClient)} + {AsyncHttpClientCall.builder().httpClientSupplier(httpClientSupplier)} }; } @@ -77,7 +92,7 @@ public class AsyncHttpClientCallTest { val numRequestCustomizer = new AtomicInteger(); // prepare http client mock - val httpClient = mock(AsyncHttpClient.class); + this.httpClient = mock(AsyncHttpClient.class); val mockRequest = mock(org.asynchttpclient.Request.class); when(mockRequest.getHeaders()).thenReturn(EmptyHttpHeaders.INSTANCE); @@ -94,13 +109,12 @@ public class AsyncHttpClientCallTest { // create call instance val call = AsyncHttpClientCall.builder() - .httpClient(httpClient) + .httpClientSupplier(httpClientSupplier) .request(REQUEST) .onRequestStart(e -> numStarted.incrementAndGet()) .onRequestFailure(t -> numFailed.incrementAndGet()) .onRequestSuccess(r -> numOk.incrementAndGet()) .requestCustomizer(rb -> numRequestCustomizer.incrementAndGet()) - .executeTimeoutMillis(1000) .build(); // when @@ -163,7 +177,7 @@ public class AsyncHttpClientCallTest { void toIOExceptionShouldProduceExpectedResult(Throwable exception) { // given val call = AsyncHttpClientCall.builder() - .httpClient(mock(AsyncHttpClient.class)) + .httpClientSupplier(httpClientSupplier) .request(REQUEST) .build(); @@ -237,13 +251,12 @@ public class AsyncHttpClientCallTest { Request request = requestWithBody(); ArgumentCaptor<org.asynchttpclient.Request> capture = ArgumentCaptor.forClass(org.asynchttpclient.Request.class); - AsyncHttpClient client = mock(AsyncHttpClient.class); - givenResponseIsProduced(client, aResponse()); + givenResponseIsProduced(httpClient, aResponse()); - whenRequestIsMade(client, request); + whenRequestIsMade(httpClient, request); - verify(client).executeRequest(capture.capture(), any()); + verify(httpClient).executeRequest(capture.capture(), any()); org.asynchttpclient.Request ahcRequest = capture.getValue(); @@ -255,11 +268,9 @@ public class AsyncHttpClientCallTest { @Test public void contenTypeIsOptionalInResponse() throws Exception { - AsyncHttpClient client = mock(AsyncHttpClient.class); + givenResponseIsProduced(httpClient, responseWithBody(null, "test")); - givenResponseIsProduced(client, responseWithBody(null, "test")); - - okhttp3.Response response = whenRequestIsMade(client, REQUEST); + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); assertEquals(response.code(), 200); assertEquals(response.header("Server"), "nginx"); @@ -269,11 +280,9 @@ public class AsyncHttpClientCallTest { @Test public void contentTypeIsProperlyParsedIfPresent() throws Exception { - AsyncHttpClient client = mock(AsyncHttpClient.class); - - givenResponseIsProduced(client, responseWithBody("text/plain", "test")); + givenResponseIsProduced(httpClient, responseWithBody("text/plain", "test")); - okhttp3.Response response = whenRequestIsMade(client, REQUEST); + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); assertEquals(response.code(), 200); assertEquals(response.header("Server"), "nginx"); @@ -284,17 +293,71 @@ public class AsyncHttpClientCallTest { @Test public void bodyIsNotNullInResponse() throws Exception { - AsyncHttpClient client = mock(AsyncHttpClient.class); - - givenResponseIsProduced(client, responseWithNoBody()); + givenResponseIsProduced(httpClient, responseWithNoBody()); - okhttp3.Response response = whenRequestIsMade(client, REQUEST); + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); assertEquals(response.code(), 200); assertEquals(response.header("Server"), "nginx"); assertNotEquals(response.body(), null); } + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*returned null.") + void getHttpClientShouldThrowISEIfSupplierReturnsNull() { + // given: + val call = AsyncHttpClientCall.builder() + .httpClientSupplier(() -> null) + .request(requestWithBody()) + .build(); + + // when: should throw ISE + call.getHttpClient(); + } + + @Test + void shouldReturnTimeoutSpecifiedInAHCInstanceConfig() { + // given: + val cfgBuilder = new DefaultAsyncHttpClientConfig.Builder(); + AsyncHttpClientConfig config = null; + + // and: setup call + val call = AsyncHttpClientCall.builder() + .httpClientSupplier(httpClientSupplier) + .request(requestWithBody()) + .build(); + + // when: set read timeout to 5s, req timeout to 6s + config = cfgBuilder.setReadTimeout((int) SECONDS.toMillis(5)) + .setRequestTimeout((int) SECONDS.toMillis(6)) + .build(); + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(6)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(6)); + + // when: set read timeout to 10 seconds, req timeout to 7s + config = cfgBuilder.setReadTimeout((int) SECONDS.toMillis(10)) + .setRequestTimeout((int) SECONDS.toMillis(7)) + .build(); + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(7)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(7)); + + // when: set request timeout to a negative value, just for fun. + config = cfgBuilder.setRequestTimeout(-1000) + .setReadTimeout(2000) + .build(); + + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout, but as positive value + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(1)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(1)); + } + private void givenResponseIsProduced(AsyncHttpClient client, Response response) { when(client.executeRequest(any(org.asynchttpclient.Request.class), any())).thenAnswer(invocation -> { AsyncCompletionHandler<Response> handler = invocation.getArgument(1); @@ -304,9 +367,11 @@ public class AsyncHttpClientCallTest { } private okhttp3.Response whenRequestIsMade(AsyncHttpClient client, Request request) throws IOException { - AsyncHttpClientCall call = AsyncHttpClientCall.builder().httpClient(client).request(request).build(); - - return call.execute(); + return AsyncHttpClientCall.builder() + .httpClientSupplier(() -> client) + .request(request) + .build() + .execute(); } private Request requestWithBody() { diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 6f475651cff5288a340c23e56e54bedc444a5672..082cdc947db848353222e4199711dbb6f36d7ba3 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,11 +3,16 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-rxjava</artifactId> <name>Asynchronous Http Client RxJava Extras</name> <description>The Async Http Client RxJava Extras.</description> + + <properties> + <javaModuleName>org.asynchttpclient.extras.rxjava</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>io.reactivex</groupId> diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index ad28f39c47a9a496f9a7225e598644682e8a76f7..6ae078a7336bf0be688bdae7f9f4fe69431f6ba5 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,11 +3,16 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-rxjava2</artifactId> <name>Asynchronous Http Client RxJava2 Extras</name> <description>The Async Http Client RxJava2 Extras.</description> + + <properties> + <javaModuleName>org.asynchttpclient.extras.rxjava2</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>io.reactivex.rxjava2</groupId> diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java index bf366f820078cffb9dffeb8c45d21ebfa50ed29f..6a5f8dca7a737723be212230023719b35cd0ee51 100644 --- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java +++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java @@ -13,6 +13,7 @@ */ package org.asynchttpclient.extras.rxjava2.maybe; +import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.reactivex.MaybeEmitter; import io.reactivex.exceptions.CompositeException; @@ -21,10 +22,14 @@ import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.HttpResponseStatus; import org.asynchttpclient.extras.rxjava2.DisposedException; +import org.asynchttpclient.netty.request.NettyRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Objects.requireNonNull; @@ -144,6 +149,76 @@ public abstract class AbstractMaybeAsyncHandlerBridge<T> implements AsyncHandler emitOnError(error); } + @Override + public void onHostnameResolutionAttempt(String name) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionAttempt(name)); + } + + @Override + public void onHostnameResolutionSuccess(String name, List<InetSocketAddress> addresses) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionSuccess(name, addresses)); + } + + @Override + public void onHostnameResolutionFailure(String name, Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionFailure(name, cause)); + } + + @Override + public void onTcpConnectAttempt(InetSocketAddress remoteAddress) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectAttempt(remoteAddress)); + } + + @Override + public void onTcpConnectSuccess(InetSocketAddress remoteAddress, Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectSuccess(remoteAddress, connection)); + } + + @Override + public void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectFailure(remoteAddress, cause)); + } + + @Override + public void onTlsHandshakeAttempt() { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeAttempt()); + } + + @Override + public void onTlsHandshakeSuccess(SSLSession sslSession) { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeSuccess(sslSession)); + } + + @Override + public void onTlsHandshakeFailure(Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeFailure(cause)); + } + + @Override + public void onConnectionPoolAttempt() { + executeUnlessEmitterDisposed(() -> delegate().onConnectionPoolAttempt()); + } + + @Override + public void onConnectionPooled(Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onConnectionPooled(connection)); + } + + @Override + public void onConnectionOffer(Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onConnectionOffer(connection)); + } + + @Override + public void onRequestSend(NettyRequest request) { + executeUnlessEmitterDisposed(() -> delegate().onRequestSend(request)); + } + + @Override + public void onRetry() { + executeUnlessEmitterDisposed(() -> delegate().onRetry()); + } + /** * Called to indicate that request processing is to be aborted because the linked Rx stream has been disposed. If * the {@link #delegate() delegate} didn't already receive a terminal event, @@ -184,4 +259,12 @@ public abstract class AbstractMaybeAsyncHandlerBridge<T> implements AsyncHandler LOGGER.debug("Not propagating onError after disposal: {}", error.getMessage(), error); } } + + private void executeUnlessEmitterDisposed(Runnable runnable) { + if (emitter.isDisposed()) { + disposed(); + } else { + runnable.run(); + } + } } diff --git a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java index b8a9b3b4e40526f19b5c9e4714e0a453ec7c1cb0..5c14778e1c77dce2c4dbe14afc8056e24e24f373 100644 --- a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java +++ b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java @@ -13,6 +13,7 @@ */ package org.asynchttpclient.extras.rxjava2.maybe; +import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.reactivex.MaybeEmitter; import io.reactivex.exceptions.CompositeException; @@ -26,7 +27,10 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -35,10 +39,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.BDDMockito.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; public class AbstractMaybeAsyncHandlerBridgeTest { @@ -57,6 +57,20 @@ public class AbstractMaybeAsyncHandlerBridgeTest { @Mock private HttpResponseBodyPart bodyPart; + private final String hostname = "service:8080"; + + @Mock + private InetSocketAddress remoteAddress; + + @Mock + private Channel channel; + + @Mock + private SSLSession sslSession; + + @Mock + private Throwable error; + @Captor private ArgumentCaptor<Throwable> throwable; @@ -76,6 +90,20 @@ public class AbstractMaybeAsyncHandlerBridgeTest { }; } + private static Runnable named(String name, Runnable runnable) { + return new Runnable() { + @Override + public String toString() { + return name; + } + + @Override + public void run() { + runnable.run(); + } + }; + } + @BeforeMethod public void initializeTest() { MockitoAnnotations.initMocks(this); @@ -104,10 +132,68 @@ public class AbstractMaybeAsyncHandlerBridgeTest { underTest.onTrailingHeadersReceived(headers); then(delegate).should().onTrailingHeadersReceived(headers); + /* when */ + underTest.onHostnameResolutionAttempt(hostname); + then(delegate).should().onHostnameResolutionAttempt(hostname); + + /* when */ + List<InetSocketAddress> remoteAddresses = Collections.singletonList(remoteAddress); + underTest.onHostnameResolutionSuccess(hostname, remoteAddresses); + then(delegate).should().onHostnameResolutionSuccess(hostname, remoteAddresses); + + /* when */ + underTest.onHostnameResolutionFailure(hostname, error); + then(delegate).should().onHostnameResolutionFailure(hostname, error); + + /* when */ + underTest.onTcpConnectAttempt(remoteAddress); + then(delegate).should().onTcpConnectAttempt(remoteAddress); + + /* when */ + underTest.onTcpConnectSuccess(remoteAddress, channel); + then(delegate).should().onTcpConnectSuccess(remoteAddress, channel); + + /* when */ + underTest.onTcpConnectFailure(remoteAddress, error); + then(delegate).should().onTcpConnectFailure(remoteAddress, error); + + /* when */ + underTest.onTlsHandshakeAttempt(); + then(delegate).should().onTlsHandshakeAttempt(); + + /* when */ + underTest.onTlsHandshakeSuccess(sslSession); + then(delegate).should().onTlsHandshakeSuccess(sslSession); + + /* when */ + underTest.onTlsHandshakeFailure(error); + then(delegate).should().onTlsHandshakeFailure(error); + + /* when */ + underTest.onConnectionPoolAttempt(); + then(delegate).should().onConnectionPoolAttempt(); + + /* when */ + underTest.onConnectionPooled(channel); + then(delegate).should().onConnectionPooled(channel); + + /* when */ + underTest.onConnectionOffer(channel); + then(delegate).should().onConnectionOffer(channel); + + /* when */ + underTest.onRequestSend(null); + then(delegate).should().onRequestSend(null); + + /* when */ + underTest.onRetry(); + then(delegate).should().onRetry(); + /* when */ underTest.onCompleted(); then(delegate).should().onCompleted(); then(emitter).should().onSuccess(this); + /* then */ verifyNoMoreInteractions(delegate); } @@ -254,6 +340,42 @@ public class AbstractMaybeAsyncHandlerBridgeTest { verifyNoMoreInteractions(delegate); } + @DataProvider + public Object[][] variousEvents() { + return new Object[][]{ + {named("onHostnameResolutionAttempt", () -> underTest.onHostnameResolutionAttempt("service:8080"))}, + {named("onHostnameResolutionSuccess", () -> underTest.onHostnameResolutionSuccess("service:8080", + Collections.singletonList(remoteAddress)))}, + {named("onHostnameResolutionFailure", () -> underTest.onHostnameResolutionFailure("service:8080", error))}, + {named("onTcpConnectAttempt", () -> underTest.onTcpConnectAttempt(remoteAddress))}, + {named("onTcpConnectSuccess", () -> underTest.onTcpConnectSuccess(remoteAddress, channel))}, + {named("onTcpConnectFailure", () -> underTest.onTcpConnectFailure(remoteAddress, error))}, + {named("onTlsHandshakeAttempt", () -> underTest.onTlsHandshakeAttempt())}, + {named("onTlsHandshakeSuccess", () -> underTest.onTlsHandshakeSuccess(sslSession))}, + {named("onTlsHandshakeFailure", () -> underTest.onTlsHandshakeFailure(error))}, + {named("onConnectionPoolAttempt", () -> underTest.onConnectionPoolAttempt())}, + {named("onConnectionPooled", () -> underTest.onConnectionPooled(channel))}, + {named("onConnectionOffer", () -> underTest.onConnectionOffer(channel))}, + {named("onRequestSend", () -> underTest.onRequestSend(null))}, + {named("onRetry", () -> underTest.onRetry())}, + }; + } + + @Test(dataProvider = "variousEvents") + public void variousEventCallbacksCheckDisposal(Runnable event) { + given(emitter.isDisposed()).willReturn(true); + + /* when */ + event.run(); + /* then */ + then(delegate).should(only()).onThrowable(isA(DisposedException.class)); + + /* when */ + event.run(); + /* then */ + verifyNoMoreInteractions(delegate); + } + private final class UnderTest extends AbstractMaybeAsyncHandlerBridge<Object> { UnderTest() { super(AbstractMaybeAsyncHandlerBridgeTest.this.emitter); diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index e91d11634b89d153f8116fba8acdfca866f1ec7e..17634f855e52acb6a6e4d3645b09ee593d1c4d47 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,9 +3,14 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-simple</artifactId> <name>Asynchronous Http Simple Client</name> <description>The Async Http Simple Client.</description> + + <properties> + <javaModuleName>org.asynchttpclient.extras.simple</javaModuleName> + </properties> + </project> diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java index 8d5bf18afebaf828018221cd3b0bd16a5924f44f..b1926b39882f4af6585bf1db77ee0b499b893c6d 100644 --- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java +++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java @@ -275,7 +275,7 @@ public class SimpleAsyncHttpClient implements Closeable { } private RequestBuilder rebuildRequest(Request rb) { - return new RequestBuilder(rb); + return rb.toBuilder(); } private Future<Response> execute(RequestBuilder rb, BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { @@ -422,7 +422,7 @@ public class SimpleAsyncHttpClient implements Closeable { } private Builder(SimpleAsyncHttpClient client) { - this.requestBuilder = new RequestBuilder(client.requestBuilder.build()); + this.requestBuilder = client.requestBuilder.build().toBuilder(); this.defaultThrowableHandler = client.defaultThrowableHandler; this.errorDocumentBehaviour = client.errorDocumentBehaviour; this.enableResumableDownload = client.resumeEnabled; diff --git a/extras/typesafeconfig/README.md b/extras/typesafeconfig/README.md index 3078cac2d51412f79b315edcc4e84d68bc01ff8f..dcc29dc2693d03114993912ace90158f6687b666 100644 --- a/extras/typesafeconfig/README.md +++ b/extras/typesafeconfig/README.md @@ -8,7 +8,7 @@ Download [the latest JAR][2] or grab via [Maven][3]: ```xml <dependency> <groupId>org.asynchttpclient</groupId> - <artifactId>async-http-client-extras-typesafeconfig</artifactId> + <artifactId>async-http-client-extras-typesafe-config</artifactId> <version>latest.version</version> </dependency> ``` @@ -16,12 +16,12 @@ Download [the latest JAR][2] or grab via [Maven][3]: or [Gradle][3]: ```groovy -compile "org.asynchttpclient:async-http-client-extras-typesafeconfig:latest.version" +compile "org.asynchttpclient:async-http-client-extras-typesafe-config:latest.version" ``` [1]: https://github.com/lightbend/config - [2]: https://search.maven.org/remote_content?g=org.asynchttpclient&a=async-http-client-extras-typesafeconfig&v=LATEST - [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.asynchttpclient%22%20a%3A%22async-http-client-extras-typesafeconfig%22 + [2]: https://search.maven.org/remote_content?g=org.asynchttpclient&a=async-http-client-extras-typesafe-config&v=LATEST + [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.asynchttpclient%22%20a%3A%22async-http-client-extras-typesafe-config%22 [snap]: https://oss.sonatype.org/content/repositories/snapshots/ ## Example usage @@ -31,4 +31,4 @@ compile "org.asynchttpclient:async-http-client-extras-typesafeconfig:latest.vers com.typesafe.config.Config config = ... AsyncHttpClientTypesafeConfig ahcConfig = new AsyncHttpClientTypesafeConfig(config); AsyncHttpClient client = new DefaultAsyncHttpClient(ahcConfig); -``` \ No newline at end of file +``` diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 34914084013686ff2c33eae450e610c033b268b0..ef0717daf4935d033840efef62cdc36b8331c7a5 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>async-http-client-extras-parent</artifactId> <groupId>org.asynchttpclient</groupId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <artifactId>async-http-client-extras-typesafe-config</artifactId> @@ -13,6 +13,7 @@ <properties> <typesafeconfig.version>1.3.3</typesafeconfig.version> + <javaModuleName>org.asynchttpclient.extras.typesafeconfig</javaModuleName> </properties> <dependencies> diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java index 8917052611469ec5ab99806f9a2e4aa476bb2258..5875c16b70d896c4f77e08800d0da7d4e300b26c 100644 --- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java +++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java @@ -69,6 +69,11 @@ public class AsyncHttpClientTypesafeConfig implements AsyncHttpClientConfig { return getIntegerOpt(MAX_CONNECTIONS_PER_HOST_CONFIG).orElse(defaultMaxConnectionsPerHost()); } + @Override + public int getAcquireFreeChannelTimeout() { + return getIntegerOpt(ACQUIRE_FREE_CHANNEL_TIMEOUT).orElse(defaultAcquireFreeChannelTimeout()); + } + @Override public int getConnectTimeout() { return getIntegerOpt(CONNECTION_TIMEOUT_CONFIG).orElse(defaultConnectTimeout()); @@ -364,6 +369,11 @@ public class AsyncHttpClientTypesafeConfig implements AsyncHttpClientConfig { return getBooleanOpt(SO_REUSE_ADDRESS_CONFIG).orElse(defaultSoReuseAddress()); } + @Override + public boolean isSoKeepAlive() { + return getBooleanOpt(SO_KEEP_ALIVE_CONFIG).orElse(defaultSoKeepAlive()); + } + @Override public int getSoLinger() { return getIntegerOpt(SO_LINGER_CONFIG).orElse(defaultSoLinger()); @@ -407,7 +417,7 @@ public class AsyncHttpClientTypesafeConfig implements AsyncHttpClientConfig { private <T> Optional<T> getOpt(Function<String, T> func, String key) { return config.hasPath(key) - ? Optional.ofNullable(func.apply(key)) - : Optional.empty(); + ? Optional.ofNullable(func.apply(key)) + : Optional.empty(); } } diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 8a8f85c87b3a8606c1538bb347b4bea6a9e1a19f..0635d4e92675e63e9ebbf2768b803799a7e9d163 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,12 +2,16 @@ <parent> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-project</artifactId> - <version>2.6.0</version> + <version>2.11.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>async-http-client-netty-utils</artifactId> <name>Asynchronous Http Client Netty Utils</name> + <properties> + <javaModuleName>org.asynchttpclient.utils</javaModuleName> + </properties> + <dependencies> <dependency> <groupId>io.netty</groupId> diff --git a/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java b/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java new file mode 100644 index 0000000000000000000000000000000000000000..4aaa61c8af5880aae267fa9943ac36a336137beb --- /dev/null +++ b/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.Charset; +import org.testng.annotations.Test; +import org.testng.Assert; +import org.testng.internal.junit.ArrayAsserts; + +public class ByteBufUtilsTests { + + @Test + public void testByteBuf2BytesEmptyByteBuf() { + ByteBuf buf = Unpooled.buffer(); + + try { + ArrayAsserts.assertArrayEquals(new byte[]{}, + ByteBufUtils.byteBuf2Bytes(buf)); + } finally { + buf.release(); + } + } + + @Test + public void testByteBuf2BytesNotEmptyByteBuf() { + ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[]{'f', 'o', 'o'}); + + try { + ArrayAsserts.assertArrayEquals(new byte[]{'f', 'o', 'o'}, + ByteBufUtils.byteBuf2Bytes(byteBuf)); + } finally { + byteBuf.release(); + } + } + + @Test + public void testByteBuf2String() { + ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[]{'f', 'o', 'o'}); + Charset charset = Charset.forName("US-ASCII"); + + try { + Assert.assertEquals( + ByteBufUtils.byteBuf2String(charset, byteBuf), "foo"); + } finally { + byteBuf.release(); + } + } + + @Test + public void testByteBuf2StringWithByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{'f'}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o', 'o'}); + + try { + Assert.assertEquals(ByteBufUtils.byteBuf2String( + Charset.forName("ISO-8859-1"), byteBuf1, byteBuf2), "foo"); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2Chars() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{}, ByteBufUtils + .byteBuf2Chars(Charset.forName("US-ASCII"), byteBuf1)); + ArrayAsserts.assertArrayEquals(new char[]{}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), byteBuf1)); + ArrayAsserts.assertArrayEquals(new char[]{'o'}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2CharsWithByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{'f', 'o'}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'%', '*'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{'f', 'o', '%', '*'}, + ByteBufUtils.byteBuf2Chars(Charset.forName("US-ASCII"), + byteBuf1, byteBuf2)); + ArrayAsserts.assertArrayEquals(new char[]{'f', 'o', '%', '*'}, + ByteBufUtils.byteBuf2Chars(Charset.forName("ISO-8859-1"), + byteBuf1, byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2CharsWithEmptyByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{'o'}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), + byteBuf1, byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } +} diff --git a/pom.xml b/pom.xml index 94f322c79740c93dafaea01bd770532b30fbec23..dc8d77b68c99e198410bbea5f90ada244bd74d9b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,31 +1,58 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <parent> - <groupId>org.sonatype.oss</groupId> - <artifactId>oss-parent</artifactId> - <version>9</version> - </parent> <modelVersion>4.0.0</modelVersion> + <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client-project</artifactId> - <name>Asynchronous Http Client Project</name> - <version>2.6.0</version> + <version>2.11.0</version> <packaging>pom</packaging> + + <name>Asynchronous Http Client Project</name> <description> The Async Http Client (AHC) library's purpose is to allow Java applications to easily execute HTTP requests and asynchronously process the response. </description> <url>http://github.com/AsyncHttpClient/async-http-client</url> + + <licenses> + <license> + <name>The Apache Software License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> + </license> + </licenses> + + <developers> + <developer> + <id>slandelle</id> + <name>Stephane Landelle</name> + <email>slandelle@gatling.io</email> + </developer> + </developers> + <scm> - <url>https://github.com/AsyncHttpClient/async-http-client</url> <connection>scm:git:git@github.com:AsyncHttpClient/async-http-client.git</connection> <developerConnection>scm:git:git@github.com:AsyncHttpClient/async-http-client.git</developerConnection> + <url>https://github.com/AsyncHttpClient/async-http-client/tree/master</url> + <tag>async-http-client-project-2.11.0</tag> </scm> + + <distributionManagement> + <snapshotRepository> + <id>sonatype-nexus-staging</id> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + </snapshotRepository> + <repository> + <id>sonatype-nexus-staging</id> + <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> + </repository> + </distributionManagement> + <issueManagement> - <system>jira</system> - <url>https://issues.sonatype.org/browse/AHC</url> + <system>github</system> + <url>https://github.com/AsyncHttpClient/async-http-client/issues</url> </issueManagement> + <mailingLists> <mailingList> <name>asynchttpclient</name> @@ -36,23 +63,6 @@ </mailingList> </mailingLists> - <prerequisites> - <maven>3.0.0</maven> - </prerequisites> - <developers> - <developer> - <id>slandelle</id> - <name>Stephane Landelle</name> - <email>slandelle@gatling.io</email> - </developer> - </developers> - <licenses> - <license> - <name>Apache License 2.0</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.html</url> - <distribution>repo</distribution> - </license> - </licenses> <build> <resources> <resource> @@ -65,20 +75,28 @@ <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ssh-external</artifactId> - <version>1.0-beta-6</version> + <version>3.3.4</version> </extension> <extension> <groupId>org.apache.maven.scm</groupId> <artifactId>maven-scm-provider-gitexe</artifactId> - <version>1.6</version> + <version>1.11.2</version> </extension> <extension> <groupId>org.apache.maven.scm</groupId> <artifactId>maven-scm-manager-plexus</artifactId> - <version>1.6</version> + <version>1.11.2</version> </extension> </extensions> <defaultGoal>install</defaultGoal> + <pluginManagement> + <plugins> + <plugin> + <artifactId>maven-release-plugin</artifactId> + <version>2.5.3</version> + </plugin> + </plugins> + </pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> @@ -136,11 +154,23 @@ </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> - <version>3.0.2</version> + <version>3.2.0</version> + <executions> + <execution> + <id>default-jar</id> + <configuration> + <archive> + <manifestEntries> + <Automatic-Module-Name>${javaModuleName}</Automatic-Module-Name> + </manifestEntries> + </archive> + </configuration> + </execution> + </executions> </plugin> <plugin> <artifactId>maven-source-plugin</artifactId> - <version>3.0.1</version> + <version>3.2.1</version> <executions> <execution> <id>attach-sources</id> @@ -151,42 +181,61 @@ </execution> </executions> </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>3.0.1</version> + <extensions>true</extensions> + <configuration> + <manifestLocation>META-INF</manifestLocation> + <instructions> + <Bundle-Version>$(replace;$(project.version);-SNAPSHOT;.$(tstamp;yyyyMMdd-HHmm))</Bundle-Version> + <Bundle-Vendor>The AsyncHttpClient Project</Bundle-Vendor> + <Import-Package>javax.activation;version="[1.1,2)", *</Import-Package> + </instructions> + </configuration> + <executions> + <execution> + <id>osgi-bundle</id> + <phase>package</phase> + <goals> + <goal>bundle</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>3.1.1</version> + <configuration> + <doclint>none</doclint> + </configuration> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-gpg-plugin</artifactId> + <version>1.6</version> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> </plugins> - <pluginManagement> - <plugins> - <plugin> - <artifactId>maven-javadoc-plugin</artifactId> - <version>2.10.4</version> - </plugin> - </plugins> - </pluginManagement> </build> <profiles> - <profile> - <id>release-sign-artifacts</id> - <activation> - <property> - <name>performRelease</name> - <value>true</value> - </property> - </activation> - <build> - <plugins> - <plugin> - <artifactId>maven-gpg-plugin</artifactId> - <executions> - <execution> - <id>sign-artifacts</id> - <phase>verify</phase> - <goals> - <goal>sign</goal> - </goals> - </execution> - </executions> - </plugin> - </plugins> - </build> - </profile> <profile> <id>test-output</id> <properties> @@ -194,20 +243,9 @@ </properties> </profile> </profiles> - <distributionManagement> - <repository> - <id>sonatype-nexus-staging</id> - <name>Sonatype Release</name> - <url>http://oss.sonatype.org/service/local/staging/deploy/maven2 - </url> - </repository> - <snapshotRepository> - <id>sonatype-nexus-snapshots</id> - <name>sonatype-nexus-snapshots</name> - <url>${distMgmtSnapshotsUrl}</url> - </snapshotRepository> - </distributionManagement> + <modules> + <module>bom</module> <module>netty-utils</module> <module>client</module> <module>extras</module> @@ -267,6 +305,13 @@ <version>${netty.version}</version> <optional>true</optional> </dependency> + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-transport-native-kqueue</artifactId> + <classifier>osx-x86_64</classifier> + <version>${netty.version}</version> + <optional>true</optional> + </dependency> <dependency> <groupId>org.reactivestreams</groupId> <artifactId>reactive-streams</artifactId> @@ -398,32 +443,31 @@ </dependency> <dependency> <groupId>org.hamcrest</groupId> - <artifactId>java-hamcrest</artifactId> + <artifactId>hamcrest</artifactId> <version>${hamcrest.version}</version> <scope>test</scope> </dependency> </dependencies> <properties> - <distMgmtSnapshotsUrl>http://oss.sonatype.org/content/repositories/snapshots</distMgmtSnapshotsUrl> <surefire.redirectTestOutputToFile>true</surefire.redirectTestOutputToFile> <source.property>1.8</source.property> <target.property>1.8</target.property> - <netty.version>4.1.30.Final</netty.version> - <slf4j.version>1.7.25</slf4j.version> - <reactive-streams.version>1.0.2</reactive-streams.version> + <netty.version>4.1.46.Final</netty.version> + <slf4j.version>1.7.30</slf4j.version> + <reactive-streams.version>1.0.3</reactive-streams.version> <activation.version>1.2.0</activation.version> - <netty-reactive-streams.version>2.0.0</netty-reactive-streams.version> + <netty-reactive-streams.version>2.0.4</netty-reactive-streams.version> <rxjava.version>1.3.8</rxjava.version> - <rxjava2.version>2.1.16</rxjava2.version> + <rxjava2.version>2.2.18</rxjava2.version> <logback.version>1.2.3</logback.version> - <testng.version>6.13.1</testng.version> - <jetty.version>9.4.11.v20180605</jetty.version> - <tomcat.version>9.0.10</tomcat.version> + <testng.version>7.1.0</testng.version> + <jetty.version>9.4.18.v20190429</jetty.version> + <tomcat.version>9.0.31</tomcat.version> <commons-io.version>2.6</commons-io.version> <commons-fileupload.version>1.3.3</commons-fileupload.version> <privilegedaccessor.version>1.2.2</privilegedaccessor.version> - <mockito.version>2.19.0</mockito.version> - <hamcrest.version>2.0.0.0</hamcrest.version> - <kerby.version>1.1.1</kerby.version> + <mockito.version>3.3.0</mockito.version> + <hamcrest.version>2.2</hamcrest.version> + <kerby.version>2.0.0</kerby.version> </properties> </project>