From d6434dbe87d6da12ce584094e8f13f47befa7fba Mon Sep 17 00:00:00 2001 From: Martin Cornejo Date: Mon, 8 May 2023 23:15:37 +0200 Subject: [PATCH] Now it's possible to configure NettyNioAsyncHttpClient in order to use a non blocking DNS resolver. --- .../bugfix-NettyNIOHTTPClient-35595eb.json | 6 + bom-internal/pom.xml | 10 + .../core/internal/util/ClassLoaderHelper.java | 3 +- http-clients/netty-nio-client/pom.xml | 9 + .../nio/netty/NettyNioAsyncHttpClient.java | 23 ++- .../http/nio/netty/SdkEventLoopGroup.java | 58 +++++- .../internal/AwaitCloseChannelPoolMap.java | 10 +- .../nio/netty/internal/BootstrapProvider.java | 18 +- .../nio/netty/internal/DnsResolverLoader.java | 62 +++++++ .../internal/SharedSdkEventLoopGroup.java | 2 +- ...nnelResolver.java => ChannelResolver.java} | 51 +++++- .../nio/netty/NettyClientTlsAuthTest.java | 19 ++ ...yNioAsyncHttpClientNonBlockingDnsTest.java | 171 ++++++++++++++++++ .../NettyNioAsyncHttpClientTestUtils.java | 148 +++++++++++++++ .../NettyNioAsyncHttpClientWireMockTest.java | 157 +++------------- .../http/nio/netty/ProxyWireMockTest.java | 24 +++ .../http/nio/netty/SdkEventLoopGroupTest.java | 15 +- .../AwaitCloseChannelPoolMapTest.java | 4 +- .../netty/internal/BootstrapProviderTest.java | 18 +- ...lverTest.java => ChannelResolverTest.java} | 4 +- .../utils/internal/ClassLoaderHelper.java | 150 +++++++++++++++ 21 files changed, 801 insertions(+), 161 deletions(-) create mode 100644 .changes/next-release/bugfix-NettyNIOHTTPClient-35595eb.json create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/DnsResolverLoader.java rename http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/{SocketChannelResolver.java => ChannelResolver.java} (50%) create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientNonBlockingDnsTest.java create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientTestUtils.java rename http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/{SocketChannelResolverTest.java => ChannelResolverTest.java} (95%) create mode 100644 utils/src/main/java/software/amazon/awssdk/utils/internal/ClassLoaderHelper.java diff --git a/.changes/next-release/bugfix-NettyNIOHTTPClient-35595eb.json b/.changes/next-release/bugfix-NettyNIOHTTPClient-35595eb.json new file mode 100644 index 000000000000..c11c4d917556 --- /dev/null +++ b/.changes/next-release/bugfix-NettyNIOHTTPClient-35595eb.json @@ -0,0 +1,6 @@ +{ + "category": "Netty NIO HTTP Client", + "contributor": "martinKindall", + "type": "bugfix", + "description": "By default, Netty threads are blocked during dns resolution, namely InetAddress.getByName is used under the hood. Now, there's an option to configure the NettyNioAsyncHttpClient in order to use a non blocking dns resolution strategy." +} diff --git a/bom-internal/pom.xml b/bom-internal/pom.xml index e5d4bc46a0eb..2ed2dc74db2c 100644 --- a/bom-internal/pom.xml +++ b/bom-internal/pom.xml @@ -134,6 +134,16 @@ netty-buffer ${netty.version} + + io.netty + netty-resolver + ${netty.version} + + + io.netty + netty-resolver-dns + ${netty.version} + org.reactivestreams reactive-streams diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/ClassLoaderHelper.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/ClassLoaderHelper.java index 3b5b50f7e9a0..2894b2bd8dc4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/ClassLoaderHelper.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/ClassLoaderHelper.java @@ -69,8 +69,7 @@ private static Class loadClassViaContext(String fqcn) { * @throws ClassNotFoundException * if failed to load the class */ - public static Class loadClass(String fqcn, Class... classes) - throws ClassNotFoundException { + public static Class loadClass(String fqcn, Class... classes) throws ClassNotFoundException { return loadClass(fqcn, true, classes); } diff --git a/http-clients/netty-nio-client/pom.xml b/http-clients/netty-nio-client/pom.xml index 7fd0ad73a057..d3009b3eb59c 100644 --- a/http-clients/netty-nio-client/pom.xml +++ b/http-clients/netty-nio-client/pom.xml @@ -85,6 +85,15 @@ io.netty netty-transport-classes-epoll + + io.netty + netty-resolver + + + io.netty + netty-resolver-dns + true + diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java index 78a3fa80fa87..6e944c68409d 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java @@ -103,6 +103,7 @@ private NettyNioAsyncHttpClient(DefaultBuilder builder, AttributeMap serviceDefa .sdkEventLoopGroup(sdkEventLoopGroup) .sslProvider(resolveSslProvider(builder)) .proxyConfiguration(builder.proxyConfiguration) + .useNonBlockingDnsResolver(builder.useNonBlockingDnsResolver) .build(); } @@ -189,7 +190,7 @@ private Duration resolveHealthCheckPingPeriod(Http2Configuration http2Configurat private SdkEventLoopGroup nonManagedEventLoopGroup(SdkEventLoopGroup eventLoopGroup) { return SdkEventLoopGroup.create(new NonManagedEventLoopGroup(eventLoopGroup.eventLoopGroup()), - eventLoopGroup.channelFactory()); + eventLoopGroup.channelFactory(), eventLoopGroup.channelType()); } @Override @@ -475,6 +476,15 @@ public interface Builder extends SdkAsyncHttpClient.Builder http2ConfigurationBuilderConsumer); + + /** + * Configure whether to use a non-blocking dns resolver or not. False by default, as netty's default dns resolver is + * blocking; it namely calls java.net.InetAddress.getByName. + *

+ * When enabled, a non-blocking dns resolver will be used instead, by modifying netty's bootstrap configuration. + * See https://netty.io/news/2016/05/26/4-1-0-Final.html + */ + Builder useNonBlockingDnsResolver(boolean useNonBlockingDnsResolver); } /** @@ -492,6 +502,7 @@ private static final class DefaultBuilder implements Builder { private Http2Configuration http2Configuration; private SslProvider sslProvider; private ProxyConfiguration proxyConfiguration; + private boolean useNonBlockingDnsResolver; private DefaultBuilder() { } @@ -716,6 +727,16 @@ public void setHttp2Configuration(Http2Configuration http2Configuration) { http2Configuration(http2Configuration); } + @Override + public Builder useNonBlockingDnsResolver(boolean useNonBlockingDnsResolver) { + this.useNonBlockingDnsResolver = useNonBlockingDnsResolver; + return this; + } + + public void setUseNonBlockingDnsResolver(boolean useNonBlockingDnsResolver) { + useNonBlockingDnsResolver(useNonBlockingDnsResolver); + } + @Override public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) { if (standardOptions.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT) == null) { diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroup.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroup.java index abb665f2c39a..75cbee568cda 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroup.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroup.java @@ -19,18 +19,20 @@ import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.util.Optional; import java.util.concurrent.ThreadFactory; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.http.nio.netty.internal.utils.SocketChannelResolver; +import software.amazon.awssdk.http.nio.netty.internal.utils.ChannelResolver; import software.amazon.awssdk.utils.ThreadFactoryBuilder; import software.amazon.awssdk.utils.Validate; /** - * Provides {@link EventLoopGroup} and {@link ChannelFactory} for {@link NettyNioAsyncHttpClient}. + * Provides {@link EventLoopGroup}, {@link ChannelFactory} and {@link DatagramChannel} for {@link NettyNioAsyncHttpClient}. *

- * There are three ways to create a new instance. + * There are four ways to create a new instance. * *

    *
  • using {@link #builder()} to provide custom configuration of {@link EventLoopGroup}. @@ -39,11 +41,15 @@ * *
  • Using {@link #create(EventLoopGroup)} to provide a custom {@link EventLoopGroup}. {@link ChannelFactory} will * be resolved based on the type of {@link EventLoopGroup} provided via - * {@link SocketChannelResolver#resolveSocketChannelFactory(EventLoopGroup)} + * {@link ChannelResolver#resolveSocketChannelFactory(EventLoopGroup)}. {@link DatagramChannel} will be resolved based + * on the type of {@link EventLoopGroup} provided via {@link ChannelResolver#resolveDatagramChannel(EventLoopGroup)} *
  • * - *
  • Using {@link #create(EventLoopGroup, ChannelFactory)} to provide a custom {@link EventLoopGroup} and - * {@link ChannelFactory} + *
  • Using {@link #create(EventLoopGroup, ChannelFactory)} to provide a custom + * {@link EventLoopGroup} and {@link ChannelFactory}
  • + * + *
  • Using {@link #create(EventLoopGroup, ChannelFactory, Class)} to provide a custom + * {@link EventLoopGroup}, {@link ChannelFactory} and {@link DatagramChannel} *
* *

@@ -63,12 +69,23 @@ public final class SdkEventLoopGroup { private final EventLoopGroup eventLoopGroup; private final ChannelFactory channelFactory; + private final Class channelType; SdkEventLoopGroup(EventLoopGroup eventLoopGroup, ChannelFactory channelFactory) { Validate.paramNotNull(eventLoopGroup, "eventLoopGroup"); Validate.paramNotNull(channelFactory, "channelFactory"); this.eventLoopGroup = eventLoopGroup; this.channelFactory = channelFactory; + this.channelType = resolveChannelType(); + } + + SdkEventLoopGroup(EventLoopGroup eventLoopGroup, ChannelFactory channelFactory, + Class channelType) { + Validate.paramNotNull(eventLoopGroup, "eventLoopGroup"); + Validate.paramNotNull(channelFactory, "channelFactory"); + this.eventLoopGroup = eventLoopGroup; + this.channelFactory = channelFactory; + this.channelType = channelType; } /** @@ -77,6 +94,7 @@ public final class SdkEventLoopGroup { private SdkEventLoopGroup(DefaultBuilder builder) { this.eventLoopGroup = resolveEventLoopGroup(builder); this.channelFactory = resolveChannelFactory(); + this.channelType = resolveChannelType(); } /** @@ -93,6 +111,13 @@ public ChannelFactory channelFactory() { return channelFactory; } + /** + * @return the {@link ChannelFactory} to be used with Netty Http Client. + */ + public Class channelType() { + return channelType; + } + /** * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup} and {@link ChannelFactory} * to be used with {@link NettyNioAsyncHttpClient}. @@ -105,6 +130,20 @@ public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup, ChannelFac return new SdkEventLoopGroup(eventLoopGroup, channelFactory); } + /** + * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup}, {@link ChannelFactory} + * and {@link DatagramChannel} to be used with {@link NettyNioAsyncHttpClient}. + * + * @param eventLoopGroup the EventLoopGroup to be used + * @param channelFactory the channel factor to be used + * @param channelType the channel type to be used + * @return a new instance of SdkEventLoopGroup + */ + public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup, ChannelFactory channelFactory, + Class channelType) { + return new SdkEventLoopGroup(eventLoopGroup, channelFactory, channelType); + } + /** * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup}. * @@ -116,7 +155,8 @@ public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup, ChannelFac * @return a new instance of SdkEventLoopGroup */ public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup) { - return create(eventLoopGroup, SocketChannelResolver.resolveSocketChannelFactory(eventLoopGroup)); + return create(eventLoopGroup, ChannelResolver.resolveSocketChannelFactory(eventLoopGroup), + ChannelResolver.resolveDatagramChannel(eventLoopGroup)); } public static Builder builder() { @@ -146,6 +186,10 @@ private ChannelFactory resolveChannelFactory() { return NioSocketChannel::new; } + private Class resolveChannelType() { + return NioDatagramChannel.class; + } + /** * A builder for {@link SdkEventLoopGroup}. * diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index 1d55e1841aa2..c86c46da87e7 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -83,6 +83,7 @@ public void channelCreated(Channel ch) throws Exception { private final ProxyConfiguration proxyConfiguration; private final BootstrapProvider bootstrapProvider; private final SslContextProvider sslContextProvider; + private final boolean useNonBlockingDnsResolver; private AwaitCloseChannelPoolMap(Builder builder, Function createBootStrapProvider) { this.configuration = builder.configuration; @@ -94,6 +95,7 @@ private AwaitCloseChannelPoolMap(Builder builder, Function init(Class channelType) { + try { + Class addressResolver = ClassLoaderHelper.loadClass(getAddressResolverGroup(), false, (Class) null); + Class dnsNameResolverBuilder = ClassLoaderHelper.loadClass(getDnsNameResolverBuilder(), false, (Class) null); + + Object dnsResolverObj = dnsNameResolverBuilder.newInstance(); + Method method = dnsResolverObj.getClass().getMethod("channelType", Class.class); + method.invoke(dnsResolverObj, channelType); + + Object e = addressResolver.getConstructor(dnsNameResolverBuilder).newInstance(dnsResolverObj); + return (AddressResolverGroup) e; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot find module io.netty.resolver.dns " + + " To use netty non blocking dns," + + " the 'netty-resolver-dns' module from io.netty must be on the class path. ", e); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { + throw new IllegalStateException("Failed to create AddressResolverGroup", e); + } + } + + private static String getAddressResolverGroup() { + return "io.netty.resolver.dns.DnsAddressResolverGroup"; + } + + private static String getDnsNameResolverBuilder() { + return "io.netty.resolver.dns.DnsNameResolverBuilder"; + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SharedSdkEventLoopGroup.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SharedSdkEventLoopGroup.java index f316b77f3d8b..b6b30ec412d7 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SharedSdkEventLoopGroup.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/SharedSdkEventLoopGroup.java @@ -59,7 +59,7 @@ public static synchronized SdkEventLoopGroup get() { referenceCount++; return SdkEventLoopGroup.create(new ReferenceCountingEventLoopGroup(sharedSdkEventLoopGroup.eventLoopGroup()), - sharedSdkEventLoopGroup.channelFactory()); + sharedSdkEventLoopGroup.channelFactory(), sharedSdkEventLoopGroup.channelType()); } /** diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolver.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolver.java similarity index 50% rename from http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolver.java rename to http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolver.java index 1d80dad5850f..0c8f512f2f17 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolver.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolver.java @@ -21,9 +21,12 @@ import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoopGroup; import io.netty.channel.ReflectiveChannelFactory; +import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.util.HashMap; import java.util.Map; @@ -31,16 +34,24 @@ import software.amazon.awssdk.http.nio.netty.internal.DelegatingEventLoopGroup; @SdkInternalApi -public final class SocketChannelResolver { +public final class ChannelResolver { - private static final Map KNOWN_EL_GROUPS = new HashMap<>(); + private static final Map KNOWN_EL_GROUPS_CHANNELS = new HashMap<>(); + private static final Map KNOWN_EL_GROUPS_DATAGRAMS = new HashMap<>(); static { - KNOWN_EL_GROUPS.put("io.netty.channel.kqueue.KQueueEventLoopGroup", "io.netty.channel.kqueue.KQueueSocketChannel"); - KNOWN_EL_GROUPS.put("io.netty.channel.oio.OioEventLoopGroup", "io.netty.channel.socket.oio.OioSocketChannel"); + KNOWN_EL_GROUPS_CHANNELS.put("io.netty.channel.kqueue.KQueueEventLoopGroup", + "io.netty.channel.kqueue.KQueueSocketChannel"); + KNOWN_EL_GROUPS_CHANNELS.put("io.netty.channel.oio.OioEventLoopGroup", + "io.netty.channel.socket.oio.OioSocketChannel"); + + KNOWN_EL_GROUPS_DATAGRAMS.put("io.netty.channel.kqueue.KQueueEventLoopGroup", + "io.netty.channel.kqueue.KQueueDatagramChannel"); + KNOWN_EL_GROUPS_DATAGRAMS.put("io.netty.channel.oio.OioEventLoopGroup", + "io.netty.channel.socket.oio.OioDatagramChannel"); } - private SocketChannelResolver() { + private ChannelResolver() { } /** @@ -63,11 +74,39 @@ public static ChannelFactory resolveSocketChannelFactory(Even return EpollSocketChannel::new; } - String socketFqcn = KNOWN_EL_GROUPS.get(eventLoopGroup.getClass().getName()); + String socketFqcn = KNOWN_EL_GROUPS_CHANNELS.get(eventLoopGroup.getClass().getName()); if (socketFqcn == null) { throw new IllegalArgumentException("Unknown event loop group : " + eventLoopGroup.getClass()); } return invokeSafely(() -> new ReflectiveChannelFactory(Class.forName(socketFqcn))); } + + /** + * Attempts to determine the {@link DatagramChannel} class that corresponds to the given + * event loop group. + * + * @param eventLoopGroup the event loop group to determine the {@link ChannelFactory} for + * @return A {@link DatagramChannel} class for the given event loop group. + */ + @SuppressWarnings("unchecked") + public static Class resolveDatagramChannel(EventLoopGroup eventLoopGroup) { + if (eventLoopGroup instanceof DelegatingEventLoopGroup) { + return resolveDatagramChannel(((DelegatingEventLoopGroup) eventLoopGroup).getDelegate()); + } + + if (eventLoopGroup instanceof NioEventLoopGroup) { + return NioDatagramChannel.class; + } + if (eventLoopGroup instanceof EpollEventLoopGroup) { + return EpollDatagramChannel.class; + } + + String datagramClass = KNOWN_EL_GROUPS_DATAGRAMS.get(eventLoopGroup.getClass().getName()); + if (datagramClass == null) { + throw new IllegalArgumentException("Unknown event loop group : " + eventLoopGroup.getClass()); + } + + return invokeSafely(() -> (Class) Class.forName(datagramClass)); + } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java index dc7c408c3c9f..f35c0914609d 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java @@ -39,6 +39,7 @@ import software.amazon.awssdk.http.EmptyPublisher; import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider; import software.amazon.awssdk.http.HttpTestUtils; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.TlsKeyManagersProvider; @@ -185,6 +186,24 @@ public void nonProxy_noKeyManagerGiven_shouldThrowException() { .hasRootCauseInstanceOf(SSLException.class); } + @Test + public void builderUsesProvidedKeyManagersProviderNonBlockingDns() { + TlsKeyManagersProvider mockKeyManagersProvider = mock(TlsKeyManagersProvider.class); + netty = NettyNioAsyncHttpClient.builder() + .useNonBlockingDnsResolver(true) + .proxyConfiguration(proxyCfg) + .tlsKeyManagersProvider(mockKeyManagersProvider) + .buildWithDefaults(AttributeMap.builder() + .put(TRUST_ALL_CERTIFICATES, true) + .build()); + + try { + sendRequest(netty, new RecordingResponseHandler()); + } catch (Exception ignored) { + } + verify(mockKeyManagersProvider).keyManagers(); + } + private void sendRequest(SdkAsyncHttpClient client, SdkAsyncHttpResponseHandler responseHandler) { AsyncExecuteRequest req = AsyncExecuteRequest.builder() .request(testSdkRequest()) diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientNonBlockingDnsTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientNonBlockingDnsTest.java new file mode 100644 index 000000000000..9535c41c2b0a --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientNonBlockingDnsTest.java @@ -0,0 +1,171 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.nio.netty; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.util.Collections.singletonMap; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.StringUtils.reverse; +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.assertCanReceiveBasicRequest; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createProvider; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createRequest; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.makeSimpleRequest; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.assertj.core.api.Condition; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.utils.AttributeMap; + +@RunWith(MockitoJUnitRunner.class) +public class NettyNioAsyncHttpClientNonBlockingDnsTest { + + private final RecordingNetworkTrafficListener wiremockTrafficListener = new RecordingNetworkTrafficListener(); + + private static final SdkAsyncHttpClient client = NettyNioAsyncHttpClient.builder() + .useNonBlockingDnsResolver(true) + .buildWithDefaults( + AttributeMap.builder() + .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) + .build()); + @Rule + public WireMockRule mockServer = new WireMockRule(wireMockConfig() + .dynamicPort() + .dynamicHttpsPort() + .networkTrafficListener(wiremockTrafficListener)); + + @Before + public void methodSetup() { + wiremockTrafficListener.reset(); + } + + @AfterClass + public static void tearDown() throws Exception { + client.close(); + } + + @Test + public void canSendContentAndGetThatContentBackNonBlockingDns() throws Exception { + String body = randomAlphabetic(50); + stubFor(any(urlEqualTo("/echo?reversed=true")) + .withRequestBody(equalTo(body)) + .willReturn(aResponse().withBody(reverse(body)))); + URI uri = URI.create("http://localhost:" + mockServer.port()); + + SdkHttpRequest request = createRequest(uri, "/echo", body, SdkHttpMethod.POST, singletonMap("reversed", "true")); + + RecordingResponseHandler recorder = new RecordingResponseHandler(); + + client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider(body)).responseHandler(recorder).build()); + + recorder.completeFuture.get(5, TimeUnit.SECONDS); + + verify(1, postRequestedFor(urlEqualTo("/echo?reversed=true"))); + + assertThat(recorder.fullResponseAsString()).isEqualTo(reverse(body)); + } + + @Test + public void defaultThreadFactoryUsesHelpfulName() throws Exception { + // Make a request to ensure a thread is primed + makeSimpleRequest(client, mockServer); + + String expectedPattern = "aws-java-sdk-NettyEventLoop-\\d+-\\d+"; + assertThat(Thread.getAllStackTraces().keySet()) + .areAtLeast(1, new Condition<>(t -> t.getName().matches(expectedPattern), + "Matches default thread pattern: `%s`", expectedPattern)); + } + + @Test + public void canMakeBasicRequestOverHttp() throws Exception { + String smallBody = randomAlphabetic(10); + URI uri = URI.create("http://localhost:" + mockServer.port()); + + assertCanReceiveBasicRequest(client, uri, smallBody); + } + + @Test + public void canMakeBasicRequestOverHttps() throws Exception { + String smallBody = randomAlphabetic(10); + URI uri = URI.create("https://localhost:" + mockServer.httpsPort()); + + assertCanReceiveBasicRequest(client, uri, smallBody); + } + + @Test + public void canHandleLargerPayloadsOverHttp() throws Exception { + String largishBody = randomAlphabetic(25000); + + URI uri = URI.create("http://localhost:" + mockServer.port()); + + assertCanReceiveBasicRequest(client, uri, largishBody); + } + + @Test + public void canHandleLargerPayloadsOverHttps() throws Exception { + String largishBody = randomAlphabetic(25000); + + URI uri = URI.create("https://localhost:" + mockServer.httpsPort()); + + assertCanReceiveBasicRequest(client, uri, largishBody); + } + + @Test + public void requestContentOnlyEqualToContentLengthHeaderFromProvider() throws InterruptedException, ExecutionException, TimeoutException, IOException { + final String content = randomAlphabetic(32); + final String streamContent = content + reverse(content); + stubFor(any(urlEqualTo("/echo?reversed=true")) + .withRequestBody(equalTo(content)) + .willReturn(aResponse().withBody(reverse(content)))); + URI uri = URI.create("http://localhost:" + mockServer.port()); + + SdkHttpFullRequest request = createRequest(uri, "/echo", streamContent, SdkHttpMethod.POST, singletonMap("reversed", "true")); + request = request.toBuilder().putHeader("Content-Length", Integer.toString(content.length())).build(); + RecordingResponseHandler recorder = new RecordingResponseHandler(); + + client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider(streamContent)).responseHandler(recorder).build()); + + recorder.completeFuture.get(5, TimeUnit.SECONDS); + + // HTTP servers will stop processing the request as soon as it reads + // bytes equal to 'Content-Length' so we need to inspect the raw + // traffic to ensure that there wasn't anything after that. + assertThat(wiremockTrafficListener.requests().toString()).endsWith(content); + } +} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientTestUtils.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientTestUtils.java new file mode 100644 index 000000000000..04f9a906ee04 --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientTestUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.nio.netty; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyMap; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.async.SdkHttpContentPublisher; + +public class NettyNioAsyncHttpClientTestUtils { + + /** + * Make a simple async request and wait for it to fiish. + * + * @param client Client to make request with. + */ + public static void makeSimpleRequest(SdkAsyncHttpClient client, WireMockServer mockServer) throws Exception { + String body = randomAlphabetic(10); + URI uri = URI.create("http://localhost:" + mockServer.port()); + stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body))); + SdkHttpRequest request = createRequest(uri); + RecordingResponseHandler recorder = new RecordingResponseHandler(); + client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build()); + recorder.completeFuture.get(5, TimeUnit.SECONDS); + } + + public static SdkHttpContentPublisher createProvider(String body) { + Stream chunks = splitStringBySize(body).stream() + .map(chunk -> ByteBuffer.wrap(chunk.getBytes(UTF_8))); + return new SdkHttpContentPublisher() { + + @Override + public Optional contentLength() { + return Optional.of(Long.valueOf(body.length())); + } + + @Override + public void subscribe(Subscriber s) { + s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + chunks.forEach(s::onNext); + s.onComplete(); + } + + @Override + public void cancel() { + + } + }); + } + }; + } + + public static SdkHttpFullRequest createRequest(URI uri) { + return createRequest(uri, "/", null, SdkHttpMethod.GET, emptyMap()); + } + + public static SdkHttpFullRequest createRequest(URI uri, + String resourcePath, + String body, + SdkHttpMethod method, + Map params) { + String contentLength = body == null ? null : String.valueOf(body.getBytes(UTF_8).length); + return SdkHttpFullRequest.builder() + .uri(uri) + .method(method) + .encodedPath(resourcePath) + .applyMutation(b -> params.forEach(b::putRawQueryParameter)) + .applyMutation(b -> { + b.putHeader("Host", uri.getHost()); + if (contentLength != null) { + b.putHeader("Content-Length", contentLength); + } + }).build(); + } + + public static void assertCanReceiveBasicRequest(SdkAsyncHttpClient client, URI uri, String body) throws Exception { + stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withHeader("Some-Header", "With Value").withBody(body))); + + SdkHttpRequest request = createRequest(uri); + + RecordingResponseHandler recorder = new RecordingResponseHandler(); + client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build()); + + recorder.completeFuture.get(5, TimeUnit.SECONDS); + + assertThat(recorder.responses).hasOnlyOneElementSatisfying( + headerResponse -> { + assertThat(headerResponse.headers()).containsKey("Some-Header"); + assertThat(headerResponse.statusCode()).isEqualTo(200); + }); + + assertThat(recorder.fullResponseAsString()).isEqualTo(body); + verify(1, getRequestedFor(urlMatching("/"))); + } + + private static Collection splitStringBySize(String str) { + if (isBlank(str)) { + return Collections.emptyList(); + } + ArrayList split = new ArrayList<>(); + for (int i = 0; i <= str.length() / 1000; i++) { + split.add(str.substring(i * 1000, Math.min((i + 1) * 1000, str.length()))); + } + return split; + } +} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java index 9a1121e201f5..79dee34357e2 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClientWireMockTest.java @@ -18,19 +18,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.any; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; -import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.reverse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -40,6 +35,10 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.assertCanReceiveBasicRequest; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createProvider; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createRequest; +import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.makeSimpleRequest; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.http.Fault; @@ -49,25 +48,22 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.SslProvider; import io.netty.util.AttributeKey; import java.io.IOException; import java.net.URI; -import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; import javax.net.ssl.TrustManagerFactory; import org.assertj.core.api.Condition; import org.junit.AfterClass; @@ -78,8 +74,6 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import software.amazon.awssdk.http.HttpMetric; import software.amazon.awssdk.http.HttpTestUtils; import software.amazon.awssdk.http.SdkHttpConfigurationOption; @@ -88,7 +82,6 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; -import software.amazon.awssdk.http.async.SdkHttpContentPublisher; import software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration; import software.amazon.awssdk.http.nio.netty.internal.SdkChannelPool; import software.amazon.awssdk.http.nio.netty.internal.SdkChannelPoolMap; @@ -183,7 +176,8 @@ public void invalidMaxPendingConnectionAcquireConfig_shouldPropagateException() .maxConcurrency(1) .maxPendingConnectionAcquires(0) .build()) { - assertThatThrownBy(() -> makeSimpleRequest(customClient)).hasMessageContaining("java.lang.IllegalArgumentException: maxPendingAcquires: 0 (expected: >= 1)"); + assertThatThrownBy(() -> makeSimpleRequest(customClient, mockServer)).hasMessageContaining("java.lang" + + ".IllegalArgumentException: maxPendingAcquires: 0 (expected: >= 1)"); } } @@ -196,7 +190,7 @@ public void customFactoryIsUsed() throws Exception { .threadFactory(threadFactory)) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); Mockito.verify(threadFactory, atLeastOnce()).newThread(Mockito.any()); @@ -208,7 +202,7 @@ public void openSslBeingUsed() throws Exception { NettyNioAsyncHttpClient.builder() .sslProvider(SslProvider.OPENSSL) .build()) { - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); } } @@ -218,7 +212,7 @@ public void defaultJdkSslProvider() throws Exception { NettyNioAsyncHttpClient.builder() .sslProvider(SslProvider.JDK) .build()) { - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); } } @@ -226,7 +220,7 @@ public void defaultJdkSslProvider() throws Exception { @Test public void defaultThreadFactoryUsesHelpfulName() throws Exception { // Make a request to ensure a thread is primed - makeSimpleRequest(client); + makeSimpleRequest(client, mockServer); String expectedPattern = "aws-java-sdk-NettyEventLoop-\\d+-\\d+"; assertThat(Thread.getAllStackTraces().keySet()) @@ -247,7 +241,7 @@ public void customThreadCountIsRespected() throws Exception { // Have to make enough requests to prime the threads for (int i = 0; i < threadCount + 1; i++) { - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); } customClient.close(); @@ -264,10 +258,10 @@ public void customEventLoopGroup_NotClosedWhenClientIsClosed() throws Exception EventLoopGroup eventLoopGroup = spy(new NioEventLoopGroup(0, threadFactory)); SdkAsyncHttpClient customClient = NettyNioAsyncHttpClient.builder() - .eventLoopGroup(SdkEventLoopGroup.create(eventLoopGroup, NioSocketChannel::new)) + .eventLoopGroup(SdkEventLoopGroup.create(eventLoopGroup, NioSocketChannel::new, NioDatagramChannel.class)) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); Mockito.verify(threadFactory, atLeastOnce()).newThread(Mockito.any()); @@ -284,10 +278,10 @@ public void customChannelFactoryIsUsed() throws Exception { SdkAsyncHttpClient customClient = NettyNioAsyncHttpClient.builder() - .eventLoopGroup(SdkEventLoopGroup.create(customEventLoopGroup, channelFactory)) + .eventLoopGroup(SdkEventLoopGroup.create(customEventLoopGroup, channelFactory, NioDatagramChannel.class)) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); Mockito.verify(channelFactory, atLeastOnce()).newChannel(); @@ -327,7 +321,7 @@ public void responseConnectionReused_shouldReleaseChannel() throws Exception { NioSocketChannel channel = new NioSocketChannel(); when(channelFactory.newChannel()).thenAnswer((Answer) invocationOnMock -> channel); - SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory); + SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory, NioDatagramChannel.class); NettyNioAsyncHttpClient customClient = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder() @@ -335,7 +329,7 @@ public void responseConnectionReused_shouldReleaseChannel() throws Exception { .maxConcurrency(1) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); verifyChannelRelease(channel); assertThat(channel.isShutdown()).isFalse(); @@ -351,7 +345,7 @@ public void connectionInactive_shouldReleaseChannel() throws Exception { NioSocketChannel channel = new NioSocketChannel(); when(channelFactory.newChannel()).thenAnswer((Answer) invocationOnMock -> channel); - SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory); + SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory, NioDatagramChannel.class); NettyNioAsyncHttpClient customClient = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder() @@ -395,7 +389,7 @@ public void responseConnectionClosed_shouldCloseAndReleaseChannel() throws Excep SdkHttpRequest request = createRequest(uri); RecordingResponseHandler recorder = new RecordingResponseHandler(); - SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory); + SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.create(customEventLoopGroup, channelFactory, NioDatagramChannel.class); NettyNioAsyncHttpClient customClient = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder() @@ -446,27 +440,12 @@ public void builderUsesProvidedTrustManagersProvider() throws Exception { } } - /** - * Make a simple async request and wait for it to fiish. - * - * @param client Client to make request with. - */ - private void makeSimpleRequest(SdkAsyncHttpClient client) throws Exception { - String body = randomAlphabetic(10); - URI uri = URI.create("http://localhost:" + mockServer.port()); - stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body))); - SdkHttpRequest request = createRequest(uri); - RecordingResponseHandler recorder = new RecordingResponseHandler(); - client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build()); - recorder.completeFuture.get(5, TimeUnit.SECONDS); - } - @Test public void canMakeBasicRequestOverHttp() throws Exception { String smallBody = randomAlphabetic(10); URI uri = URI.create("http://localhost:" + mockServer.port()); - assertCanReceiveBasicRequest(uri, smallBody); + assertCanReceiveBasicRequest(client, uri, smallBody); } @Test @@ -474,7 +453,7 @@ public void canMakeBasicRequestOverHttps() throws Exception { String smallBody = randomAlphabetic(10); URI uri = URI.create("https://localhost:" + mockServer.httpsPort()); - assertCanReceiveBasicRequest(uri, smallBody); + assertCanReceiveBasicRequest(client, uri, smallBody); } @Test @@ -483,7 +462,7 @@ public void canHandleLargerPayloadsOverHttp() throws Exception { URI uri = URI.create("http://localhost:" + mockServer.port()); - assertCanReceiveBasicRequest(uri, largishBody); + assertCanReceiveBasicRequest(client, uri, largishBody); } @Test @@ -492,7 +471,7 @@ public void canHandleLargerPayloadsOverHttps() throws Exception { URI uri = URI.create("https://localhost:" + mockServer.httpsPort()); - assertCanReceiveBasicRequest(uri, largishBody); + assertCanReceiveBasicRequest(client, uri, largishBody); } @Test @@ -563,7 +542,7 @@ public ChannelFuture close() { }; SdkAsyncHttpClient customClient = NettyNioAsyncHttpClient.builder() - .eventLoopGroup(new SdkEventLoopGroup(new NioEventLoopGroup(1), channelFactory)) + .eventLoopGroup(new SdkEventLoopGroup(new NioEventLoopGroup(1), channelFactory, NioDatagramChannel.class)) .buildWithDefaults(mapWithTrustAllCerts()); try { @@ -579,88 +558,6 @@ public ChannelFuture close() { assertThat(channelClosedFuture.get(5, TimeUnit.SECONDS)).isTrue(); } - private void assertCanReceiveBasicRequest(URI uri, String body) throws Exception { - stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withHeader("Some-Header", "With Value").withBody(body))); - - SdkHttpRequest request = createRequest(uri); - - RecordingResponseHandler recorder = new RecordingResponseHandler(); - client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider("")).responseHandler(recorder).build()); - - recorder.completeFuture.get(5, TimeUnit.SECONDS); - - assertThat(recorder.responses).hasOnlyOneElementSatisfying( - headerResponse -> { - assertThat(headerResponse.headers()).containsKey("Some-Header"); - assertThat(headerResponse.statusCode()).isEqualTo(200); - }); - - assertThat(recorder.fullResponseAsString()).isEqualTo(body); - verify(1, getRequestedFor(urlMatching("/"))); - } - - private SdkHttpContentPublisher createProvider(String body) { - Stream chunks = splitStringBySize(body).stream() - .map(chunk -> ByteBuffer.wrap(chunk.getBytes(UTF_8))); - return new SdkHttpContentPublisher() { - - @Override - public Optional contentLength() { - return Optional.of(Long.valueOf(body.length())); - } - - @Override - public void subscribe(Subscriber s) { - s.onSubscribe(new Subscription() { - @Override - public void request(long n) { - chunks.forEach(s::onNext); - s.onComplete(); - } - - @Override - public void cancel() { - - } - }); - } - }; - } - - private SdkHttpFullRequest createRequest(URI uri) { - return createRequest(uri, "/", null, SdkHttpMethod.GET, emptyMap()); - } - - private SdkHttpFullRequest createRequest(URI uri, - String resourcePath, - String body, - SdkHttpMethod method, - Map params) { - String contentLength = body == null ? null : String.valueOf(body.getBytes(UTF_8).length); - return SdkHttpFullRequest.builder() - .uri(uri) - .method(method) - .encodedPath(resourcePath) - .applyMutation(b -> params.forEach(b::putRawQueryParameter)) - .applyMutation(b -> { - b.putHeader("Host", uri.getHost()); - if (contentLength != null) { - b.putHeader("Content-Length", contentLength); - } - }).build(); - } - - private static Collection splitStringBySize(String str) { - if (isBlank(str)) { - return Collections.emptyList(); - } - ArrayList split = new ArrayList<>(); - for (int i = 0; i <= str.length() / 1000; i++) { - split.add(str.substring(i * 1000, Math.min((i + 1) * 1000, str.length()))); - } - return split; - } - // Needs to be a non-anon class in order to spy public static class CustomThreadFactory implements ThreadFactory { @Override @@ -719,7 +616,7 @@ public void createNettyClient_ReadWriteTimeoutCanBeZero() throws Exception { .writeTimeout(Duration.ZERO) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java index f797a760fdf7..438d65e1f9fc 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyWireMockTest.java @@ -126,6 +126,30 @@ public void proxyConfigured_hostInNonProxySet_doesNotConnect() { assertThat(responseHandler.fullResponseAsString()).isEqualTo("hello"); } + @Test + public void proxyConfigured_hostInNonProxySet_nonBlockingDns_doesNotConnect() { + RecordingResponseHandler responseHandler = new RecordingResponseHandler(); + AsyncExecuteRequest req = AsyncExecuteRequest.builder() + .request(testSdkRequest()) + .responseHandler(responseHandler) + .requestContentPublisher(new EmptyPublisher()) + .build(); + + ProxyConfiguration cfg = proxyCfg.toBuilder() + .nonProxyHosts(Stream.of("localhost").collect(Collectors.toSet())) + .build(); + + client = NettyNioAsyncHttpClient.builder() + .proxyConfiguration(cfg) + .useNonBlockingDnsResolver(true) + .build(); + + client.execute(req).join(); + + responseHandler.completeFuture.join(); + assertThat(responseHandler.fullResponseAsString()).isEqualTo("hello"); + } + private SdkHttpFullRequest testSdkRequest() { return SdkHttpFullRequest.builder() .method(SdkHttpMethod.GET) diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroupTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroupTest.java index a3ae76469359..a47eeee007f8 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroupTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/SdkEventLoopGroupTest.java @@ -19,6 +19,7 @@ import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; import org.junit.Test; @@ -28,13 +29,23 @@ public class SdkEventLoopGroupTest { public void creatingUsingBuilder() { SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.builder().numberOfThreads(1).build(); assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.channelType()).isNotNull(); assertThat(sdkEventLoopGroup.eventLoopGroup()).isNotNull(); } @Test - public void creatingUsingStaticMethod() { + public void creatingUsingStaticMethod_A() { SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.create(new NioEventLoopGroup(), NioSocketChannel::new); assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.channelType()).isNotNull(); + assertThat(sdkEventLoopGroup.eventLoopGroup()).isNotNull(); + } + + @Test + public void creatingUsingStaticMethod_B() { + SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.create(new NioEventLoopGroup(), NioSocketChannel::new, NioDatagramChannel.class); + assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.channelType()).isNotNull(); assertThat(sdkEventLoopGroup.eventLoopGroup()).isNotNull(); } @@ -43,6 +54,8 @@ public void notProvidingChannelFactory_channelFactoryResolved() { SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.create(new NioEventLoopGroup()); assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.channelType()).isNotNull(); + assertThat(sdkEventLoopGroup.channelType()).isNotNull(); } @Test(expected = IllegalArgumentException.class) diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java index 3b72f71be4db..6df9a60779fd 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java @@ -118,7 +118,7 @@ public void get_callsInjectedBootstrapProviderCorrectly() { channelPoolMap = new AwaitCloseChannelPoolMap(builder, null, bootstrapProvider); channelPoolMap.get(targetUri); - verify(bootstrapProvider).createBootstrap("some-awesome-service-1234.amazonaws.com", 8080); + verify(bootstrapProvider).createBootstrap("some-awesome-service-1234.amazonaws.com", 8080, false); } @Test @@ -151,7 +151,7 @@ public void get_usingProxy_callsInjectedBootstrapProviderCorrectly() { channelPoolMap = new AwaitCloseChannelPoolMap(builder, shouldProxyCache, bootstrapProvider); channelPoolMap.get(targetUri); - verify(bootstrapProvider).createBootstrap("localhost", mockProxy.port()); + verify(bootstrapProvider).createBootstrap("localhost", mockProxy.port(), false); } @Test diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BootstrapProviderTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BootstrapProviderTest.java index 337cb7ba2ec2..914587b85df3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BootstrapProviderTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BootstrapProviderTest.java @@ -42,7 +42,19 @@ public class BootstrapProviderTest { // connection attempt and not cached between connection attempts. @Test public void createBootstrap_usesUnresolvedInetSocketAddress() { - Bootstrap bootstrap = bootstrapProvider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443); + Bootstrap bootstrap = bootstrapProvider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443, false); + + SocketAddress socketAddress = bootstrap.config().remoteAddress(); + + assertThat(socketAddress).isInstanceOf(InetSocketAddress.class); + InetSocketAddress inetSocketAddress = (InetSocketAddress)socketAddress; + + assertThat(inetSocketAddress.isUnresolved()).isTrue(); + } + + @Test + public void createBootstrapNonBlockingDns_usesUnresolvedInetSocketAddress() { + Bootstrap bootstrap = bootstrapProvider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443, true); SocketAddress socketAddress = bootstrap.config().remoteAddress(); @@ -54,7 +66,7 @@ public void createBootstrap_usesUnresolvedInetSocketAddress() { @Test public void createBootstrap_defaultConfiguration_tcpKeepAliveShouldBeFalse() { - Bootstrap bootstrap = bootstrapProvider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443); + Bootstrap bootstrap = bootstrapProvider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443, false); Boolean keepAlive = (Boolean) bootstrap.config().options().get(ChannelOption.SO_KEEPALIVE); assertThat(keepAlive).isFalse(); @@ -70,7 +82,7 @@ public void createBootstrap_tcpKeepAliveTrue_shouldApply() { nettyConfiguration, new SdkChannelOptions()); - Bootstrap bootstrap = provider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443); + Bootstrap bootstrap = provider.createBootstrap("some-awesome-service-1234.amazonaws.com", 443, false); Boolean keepAlive = (Boolean) bootstrap.config().options().get(ChannelOption.SO_KEEPALIVE); assertThat(keepAlive).isTrue(); } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolverTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolverTest.java similarity index 95% rename from http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolverTest.java rename to http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolverTest.java index 472c417d4485..d7d30fe01819 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolverTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolverTest.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.http.nio.netty.internal.utils; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awssdk.http.nio.netty.internal.utils.SocketChannelResolver.resolveSocketChannelFactory; +import static software.amazon.awssdk.http.nio.netty.internal.utils.ChannelResolver.resolveSocketChannelFactory; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; @@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.nio.netty.internal.DelegatingEventLoopGroup; -public class SocketChannelResolverTest { +public class ChannelResolverTest { @Test public void canDetectFactoryForStandardNioEventLoopGroup() { diff --git a/utils/src/main/java/software/amazon/awssdk/utils/internal/ClassLoaderHelper.java b/utils/src/main/java/software/amazon/awssdk/utils/internal/ClassLoaderHelper.java new file mode 100644 index 000000000000..4cf8e91f93ae --- /dev/null +++ b/utils/src/main/java/software/amazon/awssdk/utils/internal/ClassLoaderHelper.java @@ -0,0 +1,150 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.utils.internal; + + +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public final class ClassLoaderHelper { + + private ClassLoaderHelper() { + } + + private static Class loadClassViaClasses(String fqcn, Class[] classes) { + if (classes == null) { + return null; + } + + for (Class clzz: classes) { + if (clzz == null) { + continue; + } + ClassLoader loader = clzz.getClassLoader(); + if (loader != null) { + try { + return loader.loadClass(fqcn); + } catch (ClassNotFoundException e) { + // move on to try the next class loader + } + } + } + return null; + } + + private static Class loadClassViaContext(String fqcn) { + ClassLoader loader = contextClassLoader(); + try { + return loader == null ? null : loader.loadClass(fqcn); + } catch (ClassNotFoundException e) { + // Ignored. + } + return null; + } + + /** + * Loads the class via the optionally specified classes in the order of + * their specification, and if not found, via the context class loader of + * the current thread, and if not found, from the caller class loader as the + * last resort. + * + * @param fqcn + * fully qualified class name of the target class to be loaded + * @param classes + * class loader providers + * @return the class loaded; never null + * + * @throws ClassNotFoundException + * if failed to load the class + */ + public static Class loadClass(String fqcn, Class... classes) throws ClassNotFoundException { + return loadClass(fqcn, true, classes); + } + + /** + * If classesFirst is false, loads the class via the context class + * loader of the current thread, and if not found, via the class loaders of + * the optionally specified classes in the order of their specification, and + * if not found, from the caller class loader as the + * last resort. + *

+ * If classesFirst is true, loads the class via the optionally + * specified classes in the order of their specification, and if not found, + * via the context class loader of the current thread, and if not found, + * from the caller class loader as the last resort. + * + * @param fqcn + * fully qualified class name of the target class to be loaded + * @param classesFirst + * true if the class loaders of the optionally specified classes + * take precedence over the context class loader of the current + * thread; false if the opposite is true. + * @param classes + * class loader providers + * @return the class loaded; never null + * + * @throws ClassNotFoundException if failed to load the class + */ + public static Class loadClass(String fqcn, boolean classesFirst, + Class... classes) throws ClassNotFoundException { + Class target = null; + if (classesFirst) { + target = loadClassViaClasses(fqcn, classes); + if (target == null) { + target = loadClassViaContext(fqcn); + } + } else { + target = loadClassViaContext(fqcn); + if (target == null) { + target = loadClassViaClasses(fqcn, classes); + } + } + return target == null ? Class.forName(fqcn) : target; + } + + /** + * Attempt to get the current thread's class loader and fallback to the system classloader if null + * @return a {@link ClassLoader} or null if none found + */ + private static ClassLoader contextClassLoader() { + ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (threadClassLoader != null) { + return threadClassLoader; + } + return ClassLoader.getSystemClassLoader(); + } + + /** + * Attempt to get class loader that loads the classes and fallback to the thread context classloader if null. + * + * @param classes the classes + * @return a {@link ClassLoader} or null if none found + */ + public static ClassLoader classLoader(Class... classes) { + if (classes != null) { + for (Class clzz : classes) { + ClassLoader classLoader = clzz.getClassLoader(); + + if (classLoader != null) { + return classLoader; + } + } + } + + return contextClassLoader(); + } + +} \ No newline at end of file