diff --git a/.brazil.json b/.brazil.json index 9059e5646031..1f0931d0747b 100644 --- a/.brazil.json +++ b/.brazil.json @@ -109,6 +109,7 @@ "io.netty:netty-common": { "packageName": "Netty4", "packageVersion": "4.1" }, "io.netty:netty-handler": { "packageName": "Netty4", "packageVersion": "4.1" }, "io.netty:netty-resolver": { "packageName": "Netty4", "packageVersion": "4.1" }, + "io.netty:netty-resolver-dns": { "packageName": "Netty4", "packageVersion": "4.1" }, "io.netty:netty-transport": { "packageName": "Netty4", "packageVersion": "4.1" }, "io.netty:netty-transport-classes-epoll": { "packageName": "Netty4", "packageVersion": "4.1" }, "io.netty:netty-transport-native-unix-common": { "packageName": "Netty4", "packageVersion": "4.1" }, 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 ae8cfad8e50b..c62174a1af03 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 1486cb0ac7d8..42872c09849c 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..c12aeab10180 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(); } @@ -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..254211e9303f 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,11 +19,13 @@ 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; @@ -39,7 +41,8 @@ * *

  • 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)} and + * {@link ChannelResolver#resolveDatagramChannelFactory(EventLoopGroup)} *
  • * *
  • Using {@link #create(EventLoopGroup, ChannelFactory)} to provide a custom {@link EventLoopGroup} and @@ -63,12 +66,14 @@ public final class SdkEventLoopGroup { private final EventLoopGroup eventLoopGroup; private final ChannelFactory channelFactory; + private final ChannelFactory datagramChannelFactory; SdkEventLoopGroup(EventLoopGroup eventLoopGroup, ChannelFactory channelFactory) { Validate.paramNotNull(eventLoopGroup, "eventLoopGroup"); Validate.paramNotNull(channelFactory, "channelFactory"); this.eventLoopGroup = eventLoopGroup; this.channelFactory = channelFactory; + this.datagramChannelFactory = ChannelResolver.resolveDatagramChannelFactory(eventLoopGroup); } /** @@ -76,7 +81,8 @@ public final class SdkEventLoopGroup { */ private SdkEventLoopGroup(DefaultBuilder builder) { this.eventLoopGroup = resolveEventLoopGroup(builder); - this.channelFactory = resolveChannelFactory(); + this.channelFactory = resolveSocketChannelFactory(builder); + this.datagramChannelFactory = resolveDatagramChannelFactory(builder); } /** @@ -93,6 +99,13 @@ public ChannelFactory channelFactory() { return channelFactory; } + /** + * @return the {@link ChannelFactory} for datagram channels to be used with Netty Http Client. + */ + public ChannelFactory datagramChannelFactory() { + return datagramChannelFactory; + } + /** * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup} and {@link ChannelFactory} * to be used with {@link NettyNioAsyncHttpClient}. @@ -116,7 +129,7 @@ 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)); } public static Builder builder() { @@ -141,11 +154,22 @@ private EventLoopGroup resolveEventLoopGroup(DefaultBuilder builder) { }*/ } - private ChannelFactory resolveChannelFactory() { - // Currently we only support NioEventLoopGroup + private ChannelFactory resolveSocketChannelFactory(DefaultBuilder builder) { + return builder.channelFactory; + } + + private ChannelFactory resolveDatagramChannelFactory(DefaultBuilder builder) { + return builder.datagramChannelFactory; + } + + private static ChannelFactory defaultSocketChannelFactory() { return NioSocketChannel::new; } + private static ChannelFactory defaultDatagramChannelFactory() { + return NioDatagramChannel::new; + } + /** * A builder for {@link SdkEventLoopGroup}. * @@ -172,6 +196,24 @@ public interface Builder { */ Builder threadFactory(ThreadFactory threadFactory); + /** + * {@link ChannelFactory} to create socket channels used by the {@link EventLoopGroup}. If not set, + * NioSocketChannel is used. + * + * @param channelFactory ChannelFactory to use. + * @return This builder for method chaining. + */ + Builder channelFactory(ChannelFactory channelFactory); + + /** + * {@link ChannelFactory} to create datagram channels used by the {@link EventLoopGroup}. If not set, + * NioDatagramChannel is used. + * + * @param datagramChannelFactory ChannelFactory to use. + * @return This builder for method chaining. + */ + Builder datagramChannelFactory(ChannelFactory datagramChannelFactory); + SdkEventLoopGroup build(); } @@ -179,6 +221,8 @@ private static final class DefaultBuilder implements Builder { private Integer numberOfThreads; private ThreadFactory threadFactory; + private ChannelFactory channelFactory = defaultSocketChannelFactory(); + private ChannelFactory datagramChannelFactory = defaultDatagramChannelFactory(); private DefaultBuilder() { } @@ -203,6 +247,26 @@ public void setThreadFactory(ThreadFactory threadFactory) { threadFactory(threadFactory); } + @Override + public Builder channelFactory(ChannelFactory channelFactory) { + this.channelFactory = channelFactory; + return this; + } + + public void setChannelFactory(ChannelFactory channelFactory) { + channelFactory(channelFactory); + } + + @Override + public Builder datagramChannelFactory(ChannelFactory datagramChannelFactory) { + this.datagramChannelFactory = datagramChannelFactory; + return this; + } + + public void setDatagramChannelFactory(ChannelFactory datagramChannelFactory) { + datagramChannelFactory(datagramChannelFactory); + } + @Override public SdkEventLoopGroup build() { return new SdkEventLoopGroup(this); 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..fbd727239239 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(ChannelFactory datagramChannelFactory) { + 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("channelFactory", ChannelFactory.class); + method.invoke(dnsResolverObj, datagramChannelFactory); + + 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/utils/ChannelResolver.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolver.java new file mode 100644 index 000000000000..8770d683a679 --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/ChannelResolver.java @@ -0,0 +1,112 @@ +/* + * 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.internal.utils; + +import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; + +import io.netty.channel.Channel; +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; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.nio.netty.internal.DelegatingEventLoopGroup; + +@SdkInternalApi +public final class ChannelResolver { + + private static final Map KNOWN_EL_GROUPS_SOCKET_CHANNELS = new HashMap<>(); + private static final Map KNOWN_EL_GROUPS_DATAGRAM_CHANNELS = new HashMap<>(); + + static { + KNOWN_EL_GROUPS_SOCKET_CHANNELS.put("io.netty.channel.kqueue.KQueueEventLoopGroup", + "io.netty.channel.kqueue.KQueueSocketChannel"); + KNOWN_EL_GROUPS_SOCKET_CHANNELS.put("io.netty.channel.oio.OioEventLoopGroup", + "io.netty.channel.socket.oio.OioSocketChannel"); + + KNOWN_EL_GROUPS_DATAGRAM_CHANNELS.put("io.netty.channel.kqueue.KQueueEventLoopGroup", + "io.netty.channel.kqueue.KQueueDatagramChannel"); + KNOWN_EL_GROUPS_DATAGRAM_CHANNELS.put("io.netty.channel.oio.OioEventLoopGroup", + "io.netty.channel.socket.oio.OioDatagramChannel"); + } + + private ChannelResolver() { + } + + /** + * Attempts to determine the {@link ChannelFactory} class that corresponds to the given + * event loop group. + * + * @param eventLoopGroup the event loop group to determine the {@link ChannelFactory} for + * @return A {@link ChannelFactory} instance for the given event loop group. + */ + @SuppressWarnings("unchecked") + public static ChannelFactory resolveSocketChannelFactory(EventLoopGroup eventLoopGroup) { + if (eventLoopGroup instanceof DelegatingEventLoopGroup) { + return resolveSocketChannelFactory(((DelegatingEventLoopGroup) eventLoopGroup).getDelegate()); + } + + if (eventLoopGroup instanceof NioEventLoopGroup) { + return NioSocketChannel::new; + } + if (eventLoopGroup instanceof EpollEventLoopGroup) { + return EpollSocketChannel::new; + } + + String socketFqcn = KNOWN_EL_GROUPS_SOCKET_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 ChannelFactory} class for datagram channels that corresponds to the given + * event loop group. + * + * @param eventLoopGroup the event loop group to determine the {@link ChannelFactory} for + * @return A {@link ChannelFactory} instance for the given event loop group. + */ + @SuppressWarnings("unchecked") + public static ChannelFactory resolveDatagramChannelFactory(EventLoopGroup eventLoopGroup) { + if (eventLoopGroup instanceof DelegatingEventLoopGroup) { + return resolveDatagramChannelFactory(((DelegatingEventLoopGroup) eventLoopGroup).getDelegate()); + } + + if (eventLoopGroup instanceof NioEventLoopGroup) { + return NioDatagramChannel::new; + } + if (eventLoopGroup instanceof EpollEventLoopGroup) { + return EpollDatagramChannel::new; + } + + String datagramFqcn = KNOWN_EL_GROUPS_DATAGRAM_CHANNELS.get(eventLoopGroup.getClass().getName()); + if (datagramFqcn == null) { + throw new IllegalArgumentException("Unknown event loop group : " + eventLoopGroup.getClass()); + } + + return invokeSafely(() -> new ReflectiveChannelFactory(Class.forName(datagramFqcn))); + } +} 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/SocketChannelResolver.java deleted file mode 100644 index 1d80dad5850f..000000000000 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/utils/SocketChannelResolver.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.internal.utils; - -import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelFactory; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.ReflectiveChannelFactory; -import io.netty.channel.epoll.EpollEventLoopGroup; -import io.netty.channel.epoll.EpollSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import java.util.HashMap; -import java.util.Map; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.nio.netty.internal.DelegatingEventLoopGroup; - -@SdkInternalApi -public final class SocketChannelResolver { - - private static final Map KNOWN_EL_GROUPS = 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"); - } - - private SocketChannelResolver() { - } - - /** - * Attempts to determine the {@link ChannelFactory} class that corresponds to the given - * event loop group. - * - * @param eventLoopGroup the event loop group to determine the {@link ChannelFactory} for - * @return A {@link ChannelFactory} instance for the given event loop group. - */ - @SuppressWarnings("unchecked") - public static ChannelFactory resolveSocketChannelFactory(EventLoopGroup eventLoopGroup) { - if (eventLoopGroup instanceof DelegatingEventLoopGroup) { - return resolveSocketChannelFactory(((DelegatingEventLoopGroup) eventLoopGroup).getDelegate()); - } - - if (eventLoopGroup instanceof NioEventLoopGroup) { - return NioSocketChannel::new; - } - if (eventLoopGroup instanceof EpollEventLoopGroup) { - return EpollSocketChannel::new; - } - - String socketFqcn = KNOWN_EL_GROUPS.get(eventLoopGroup.getClass().getName()); - if (socketFqcn == null) { - throw new IllegalArgumentException("Unknown event loop group : " + eventLoopGroup.getClass()); - } - - return invokeSafely(() -> new ReflectiveChannelFactory(Class.forName(socketFqcn))); - } -} 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..116119d36ea5 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(); @@ -267,7 +261,7 @@ public void customEventLoopGroup_NotClosedWhenClientIsClosed() throws Exception .eventLoopGroup(SdkEventLoopGroup.create(eventLoopGroup, NioSocketChannel::new)) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); Mockito.verify(threadFactory, atLeastOnce()).newThread(Mockito.any()); @@ -287,7 +281,7 @@ public void customChannelFactoryIsUsed() throws Exception { .eventLoopGroup(SdkEventLoopGroup.create(customEventLoopGroup, channelFactory)) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); customClient.close(); Mockito.verify(channelFactory, atLeastOnce()).newChannel(); @@ -335,7 +329,7 @@ public void responseConnectionReused_shouldReleaseChannel() throws Exception { .maxConcurrency(1) .build(); - makeSimpleRequest(customClient); + makeSimpleRequest(customClient, mockServer); verifyChannelRelease(channel); assertThat(channel.isShutdown()).isFalse(); @@ -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 @@ -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..bb2598345cff 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 @@ -18,8 +18,15 @@ import static org.assertj.core.api.Assertions.assertThat; import io.netty.channel.DefaultEventLoopGroup; +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.oio.OioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.socket.oio.OioDatagramChannel; +import io.netty.channel.socket.oio.OioSocketChannel; import org.junit.Test; public class SdkEventLoopGroupTest { @@ -28,13 +35,24 @@ public class SdkEventLoopGroupTest { public void creatingUsingBuilder() { SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.builder().numberOfThreads(1).build(); assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.datagramChannelFactory()).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.datagramChannelFactory().newChannel()).isInstanceOf(NioDatagramChannel.class); + assertThat(sdkEventLoopGroup.eventLoopGroup()).isNotNull(); + } + + @Test + public void creatingUsingStaticMethod_B() { + SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.create(new OioEventLoopGroup(), OioSocketChannel::new); + assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.datagramChannelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.datagramChannelFactory().newChannel()).isInstanceOf(OioDatagramChannel.class); assertThat(sdkEventLoopGroup.eventLoopGroup()).isNotNull(); } @@ -43,6 +61,7 @@ public void notProvidingChannelFactory_channelFactoryResolved() { SdkEventLoopGroup sdkEventLoopGroup = SdkEventLoopGroup.create(new NioEventLoopGroup()); assertThat(sdkEventLoopGroup.channelFactory()).isNotNull(); + assertThat(sdkEventLoopGroup.datagramChannelFactory().newChannel()).isInstanceOf(NioDatagramChannel.class); } @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..17289d1ca3b3 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, null); } @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(), null); } @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/DnsResolverLoaderTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/DnsResolverLoaderTest.java new file mode 100644 index 000000000000..40db804aacaf --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/DnsResolverLoaderTest.java @@ -0,0 +1,34 @@ +/* + * 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.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.oio.OioDatagramChannel; +import io.netty.resolver.dns.DnsAddressResolverGroup; +import org.junit.jupiter.api.Test; + +public class DnsResolverLoaderTest { + + @Test + public void canResolveChannelFactory() { + assertThat(DnsResolverLoader.init(NioDatagramChannel::new)).isInstanceOf(DnsAddressResolverGroup.class); + assertThat(DnsResolverLoader.init(EpollDatagramChannel::new)).isInstanceOf(DnsAddressResolverGroup.class); + assertThat(DnsResolverLoader.init(OioDatagramChannel::new)).isInstanceOf(DnsAddressResolverGroup.class); + } +} 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 70% 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..45edd2b81bb1 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,39 +16,47 @@ 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.resolveDatagramChannelFactory; +import static software.amazon.awssdk.http.nio.netty.internal.utils.ChannelResolver.resolveSocketChannelFactory; import io.netty.channel.epoll.Epoll; +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.oio.OioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.socket.oio.OioDatagramChannel; import io.netty.channel.socket.oio.OioSocketChannel; import org.junit.jupiter.api.Assumptions; 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() { assertThat(resolveSocketChannelFactory(new NioEventLoopGroup()).newChannel()).isInstanceOf(NioSocketChannel.class); + assertThat(resolveDatagramChannelFactory(new NioEventLoopGroup()).newChannel()).isInstanceOf(NioDatagramChannel.class); } @Test public void canDetectEpollEventLoopGroupFactory() { Assumptions.assumeTrue(Epoll.isAvailable()); assertThat(resolveSocketChannelFactory(new EpollEventLoopGroup()).newChannel()).isInstanceOf(EpollSocketChannel.class); + assertThat(resolveDatagramChannelFactory(new EpollEventLoopGroup()).newChannel()).isInstanceOf(EpollDatagramChannel.class); } @Test public void worksWithDelegateEventLoopGroupsFactory() { assertThat(resolveSocketChannelFactory(new DelegatingEventLoopGroup(new NioEventLoopGroup()) {}).newChannel()).isInstanceOf(NioSocketChannel.class); + assertThat(resolveDatagramChannelFactory(new DelegatingEventLoopGroup(new NioEventLoopGroup()) {}).newChannel()).isInstanceOf(NioDatagramChannel.class); } @Test public void worksWithOioEventLoopGroupFactory() { assertThat(resolveSocketChannelFactory(new OioEventLoopGroup()).newChannel()).isInstanceOf(OioSocketChannel.class); + assertThat(resolveDatagramChannelFactory(new OioEventLoopGroup()).newChannel()).isInstanceOf(OioDatagramChannel.class); } } diff --git a/utils/src/main/java/software/amazon/awssdk/utils/ClassLoaderHelper.java b/utils/src/main/java/software/amazon/awssdk/utils/ClassLoaderHelper.java new file mode 100644 index 000000000000..487ae82df18d --- /dev/null +++ b/utils/src/main/java/software/amazon/awssdk/utils/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; + + +import software.amazon.awssdk.annotations.SdkProtectedApi; + +@SdkProtectedApi +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