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 extends Channel> channelFactory;
+ private final ChannelFactory extends DatagramChannel> datagramChannelFactory;
SdkEventLoopGroup(EventLoopGroup eventLoopGroup, ChannelFactory extends Channel> 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 extends Channel> channelFactory() {
return channelFactory;
}
+ /**
+ * @return the {@link ChannelFactory} for datagram channels to be used with Netty Http Client.
+ */
+ public ChannelFactory extends DatagramChannel> 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 extends Channel> resolveChannelFactory() {
- // Currently we only support NioEventLoopGroup
+ private ChannelFactory extends Channel> resolveSocketChannelFactory(DefaultBuilder builder) {
+ return builder.channelFactory;
+ }
+
+ private ChannelFactory extends DatagramChannel> resolveDatagramChannelFactory(DefaultBuilder builder) {
+ return builder.datagramChannelFactory;
+ }
+
+ private static ChannelFactory extends Channel> defaultSocketChannelFactory() {
return NioSocketChannel::new;
}
+ private static ChannelFactory extends DatagramChannel> 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 extends Channel> 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 extends DatagramChannel> datagramChannelFactory);
+
SdkEventLoopGroup build();
}
@@ -179,6 +221,8 @@ private static final class DefaultBuilder implements Builder {
private Integer numberOfThreads;
private ThreadFactory threadFactory;
+ private ChannelFactory extends Channel> channelFactory = defaultSocketChannelFactory();
+ private ChannelFactory extends DatagramChannel> datagramChannelFactory = defaultDatagramChannelFactory();
private DefaultBuilder() {
}
@@ -203,6 +247,26 @@ public void setThreadFactory(ThreadFactory threadFactory) {
threadFactory(threadFactory);
}
+ @Override
+ public Builder channelFactory(ChannelFactory extends Channel> channelFactory) {
+ this.channelFactory = channelFactory;
+ return this;
+ }
+
+ public void setChannelFactory(ChannelFactory extends Channel> channelFactory) {
+ channelFactory(channelFactory);
+ }
+
+ @Override
+ public Builder datagramChannelFactory(ChannelFactory extends DatagramChannel> datagramChannelFactory) {
+ this.datagramChannelFactory = datagramChannelFactory;
+ return this;
+ }
+
+ public void setDatagramChannelFactory(ChannelFactory extends DatagramChannel> 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 extends DatagramChannel> 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 extends Channel> 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 extends DatagramChannel> 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 extends Channel> 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 super ByteBuffer> 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 super ByteBuffer> 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