Skip to content

Commit 8dc8fe6

Browse files
committed
Now it's possible to configure NettyNioAsyncHttpClient in order to use a
non blocking DNS resolver.
1 parent f12ccc7 commit 8dc8fe6

File tree

13 files changed

+502
-124
lines changed

13 files changed

+502
-124
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Netty NIO HTTP Client",
3+
"contributor": "martinKindall",
4+
"type": "bugfix",
5+
"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."
6+
}

bom-internal/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@
134134
<artifactId>netty-buffer</artifactId>
135135
<version>${netty.version}</version>
136136
</dependency>
137+
<dependency>
138+
<groupId>io.netty</groupId>
139+
<artifactId>netty-resolver</artifactId>
140+
<version>${netty.version}</version>
141+
</dependency>
142+
<dependency>
143+
<groupId>io.netty</groupId>
144+
<artifactId>netty-resolver-dns</artifactId>
145+
<version>${netty.version}</version>
146+
</dependency>
137147
<dependency>
138148
<groupId>org.reactivestreams</groupId>
139149
<artifactId>reactive-streams</artifactId>

http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ public final class SdkHttpConfigurationOption<T> extends AttributeMap.Key<T> {
131131
public static final SdkHttpConfigurationOption<Duration> TLS_NEGOTIATION_TIMEOUT =
132132
new SdkHttpConfigurationOption<>("TlsNegotiationTimeout", Duration.class);
133133

134+
/**
135+
* Configure whether to use a non-blocking dns resolver or not. False by default, as netty's default dns resolver is
136+
* blocking; it namely calls java.net.InetAddress.getByName.
137+
* <p>
138+
* When enabled, a non-blocking dns resolver will be used instead, by modifying netty's bootstrap configuration.
139+
* See https://netty.io/news/2016/05/26/4-1-0-Final.html
140+
*/
141+
public static final SdkHttpConfigurationOption<Boolean> USE_NONBLOCKING_DNS_RESOLVER =
142+
new SdkHttpConfigurationOption<>("UseNonBlockingDnsResolver", Boolean.class);
143+
134144
private static final Duration DEFAULT_SOCKET_READ_TIMEOUT = Duration.ofSeconds(30);
135145
private static final Duration DEFAULT_SOCKET_WRITE_TIMEOUT = Duration.ofSeconds(30);
136146
private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(2);
@@ -152,6 +162,8 @@ public final class SdkHttpConfigurationOption<T> extends AttributeMap.Key<T> {
152162
private static final TlsTrustManagersProvider DEFAULT_TLS_TRUST_MANAGERS_PROVIDER = null;
153163
private static final TlsKeyManagersProvider DEFAULT_TLS_KEY_MANAGERS_PROVIDER = SystemPropertyTlsKeyManagersProvider.create();
154164

165+
private static final Boolean DEFAULT_USE_NONBLOCKING_DNS_RESOLVER = Boolean.FALSE;
166+
155167
public static final AttributeMap GLOBAL_HTTP_DEFAULTS = AttributeMap
156168
.builder()
157169
.put(READ_TIMEOUT, DEFAULT_SOCKET_READ_TIMEOUT)
@@ -169,6 +181,7 @@ public final class SdkHttpConfigurationOption<T> extends AttributeMap.Key<T> {
169181
.put(TLS_KEY_MANAGERS_PROVIDER, DEFAULT_TLS_KEY_MANAGERS_PROVIDER)
170182
.put(TLS_TRUST_MANAGERS_PROVIDER, DEFAULT_TLS_TRUST_MANAGERS_PROVIDER)
171183
.put(TLS_NEGOTIATION_TIMEOUT, DEFAULT_TLS_NEGOTIATION_TIMEOUT)
184+
.put(USE_NONBLOCKING_DNS_RESOLVER, DEFAULT_USE_NONBLOCKING_DNS_RESOLVER)
172185
.build();
173186

174187
private final String name;

http-clients/netty-nio-client/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@
8585
<groupId>io.netty</groupId>
8686
<artifactId>netty-transport-classes-epoll</artifactId>
8787
</dependency>
88+
<dependency>
89+
<groupId>io.netty</groupId>
90+
<artifactId>netty-resolver</artifactId>
91+
</dependency>
92+
<dependency>
93+
<groupId>io.netty</groupId>
94+
<artifactId>netty-resolver-dns</artifactId>
95+
</dependency>
8896

8997
<!--Reactive Dependencies-->
9098
<dependency>

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,15 @@ public interface Builder extends SdkAsyncHttpClient.Builder<NettyNioAsyncHttpCli
475475
* @return the builder for method chaining.
476476
*/
477477
Builder http2Configuration(Consumer<Http2Configuration.Builder> http2ConfigurationBuilderConsumer);
478+
479+
/**
480+
* Configure whether to use a non-blocking dns resolver or not. False by default, as netty's default dns resolver is
481+
* blocking; it namely calls java.net.InetAddress.getByName.
482+
* <p>
483+
* When enabled, a non-blocking dns resolver will be used instead, by modifying netty's bootstrap configuration.
484+
* See https://netty.io/news/2016/05/26/4-1-0-Final.html
485+
*/
486+
Builder useNonBlockingDnsResolver(Boolean useNonBlockingDnsResolver);
478487
}
479488

480489
/**
@@ -716,6 +725,16 @@ public void setHttp2Configuration(Http2Configuration http2Configuration) {
716725
http2Configuration(http2Configuration);
717726
}
718727

728+
@Override
729+
public Builder useNonBlockingDnsResolver(Boolean useNonBlockingDnsResolver) {
730+
standardOptions.put(SdkHttpConfigurationOption.USE_NONBLOCKING_DNS_RESOLVER, useNonBlockingDnsResolver);
731+
return this;
732+
}
733+
734+
public void setUseNonBlockingDnsResolver(Boolean useNonBlockingDnsResolver) {
735+
useNonBlockingDnsResolver(useNonBlockingDnsResolver);
736+
}
737+
719738
@Override
720739
public SdkAsyncHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
721740
if (standardOptions.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT) == null) {

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BootstrapProvider.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
import io.netty.bootstrap.Bootstrap;
1919
import io.netty.channel.ChannelOption;
20+
import io.netty.channel.socket.nio.NioDatagramChannel;
21+
import io.netty.resolver.AddressResolverGroup;
22+
import io.netty.resolver.dns.DnsAddressResolverGroup;
23+
import io.netty.resolver.dns.DnsNameResolverBuilder;
24+
import io.netty.resolver.dns.NoopDnsCache;
2025
import java.net.InetSocketAddress;
2126
import software.amazon.awssdk.annotations.SdkInternalApi;
2227
import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup;
@@ -56,8 +61,21 @@ public Bootstrap createBootstrap(String host, int port) {
5661
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyConfiguration.connectTimeoutMillis())
5762
.option(ChannelOption.SO_KEEPALIVE, nettyConfiguration.tcpKeepAlive())
5863
.remoteAddress(InetSocketAddress.createUnresolved(host, port));
64+
65+
if (nettyConfiguration.isNonBlockingResolver()) {
66+
bootstrap.resolver(nonBlockingResolverGroup());
67+
}
68+
5969
sdkChannelOptions.channelOptions().forEach(bootstrap::option);
6070

6171
return bootstrap;
6272
}
73+
74+
private AddressResolverGroup<InetSocketAddress> nonBlockingResolverGroup() {
75+
DnsNameResolverBuilder builder = new DnsNameResolverBuilder()
76+
.channelType(NioDatagramChannel.class)
77+
.resolveCache(NoopDnsCache.INSTANCE);
78+
79+
return new DnsAddressResolverGroup(builder);
80+
}
6381
}

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NettyConfiguration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,8 @@ public boolean tcpKeepAlive() {
107107
public Duration tlsHandshakeTimeout() {
108108
return configuration.get(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT);
109109
}
110+
111+
public boolean isNonBlockingResolver() {
112+
return configuration.get(SdkHttpConfigurationOption.USE_NONBLOCKING_DNS_RESOLVER);
113+
}
110114
}

http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/NettyClientTlsAuthTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import software.amazon.awssdk.http.EmptyPublisher;
4040
import software.amazon.awssdk.http.FileStoreTlsKeyManagersProvider;
4141
import software.amazon.awssdk.http.HttpTestUtils;
42+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
4243
import software.amazon.awssdk.http.SdkHttpFullRequest;
4344
import software.amazon.awssdk.http.SdkHttpMethod;
4445
import software.amazon.awssdk.http.TlsKeyManagersProvider;
@@ -185,6 +186,24 @@ public void nonProxy_noKeyManagerGiven_shouldThrowException() {
185186
.hasRootCauseInstanceOf(SSLException.class);
186187
}
187188

189+
@Test
190+
public void builderUsesProvidedKeyManagersProviderNonBlockingDns() {
191+
TlsKeyManagersProvider mockKeyManagersProvider = mock(TlsKeyManagersProvider.class);
192+
netty = NettyNioAsyncHttpClient.builder()
193+
.proxyConfiguration(proxyCfg)
194+
.tlsKeyManagersProvider(mockKeyManagersProvider)
195+
.buildWithDefaults(AttributeMap.builder()
196+
.put(TRUST_ALL_CERTIFICATES, true)
197+
.put(SdkHttpConfigurationOption.USE_NONBLOCKING_DNS_RESOLVER, true)
198+
.build());
199+
200+
try {
201+
sendRequest(netty, new RecordingResponseHandler());
202+
} catch (Exception ignored) {
203+
}
204+
verify(mockKeyManagersProvider).keyManagers();
205+
}
206+
188207
private void sendRequest(SdkAsyncHttpClient client, SdkAsyncHttpResponseHandler responseHandler) {
189208
AsyncExecuteRequest req = AsyncExecuteRequest.builder()
190209
.request(testSdkRequest())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.nio.netty;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.any;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
23+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
24+
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
25+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
26+
import static java.util.Collections.singletonMap;
27+
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
28+
import static org.apache.commons.lang3.StringUtils.reverse;
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.assertCanReceiveBasicRequest;
31+
import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createProvider;
32+
import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.createRequest;
33+
import static software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClientTestUtils.makeSimpleRequest;
34+
35+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
36+
import java.io.IOException;
37+
import java.net.URI;
38+
import java.util.concurrent.ExecutionException;
39+
import java.util.concurrent.TimeUnit;
40+
import java.util.concurrent.TimeoutException;
41+
import org.assertj.core.api.Condition;
42+
import org.junit.AfterClass;
43+
import org.junit.Before;
44+
import org.junit.Rule;
45+
import org.junit.Test;
46+
import org.junit.runner.RunWith;
47+
import org.mockito.junit.MockitoJUnitRunner;
48+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
49+
import software.amazon.awssdk.http.SdkHttpFullRequest;
50+
import software.amazon.awssdk.http.SdkHttpMethod;
51+
import software.amazon.awssdk.http.SdkHttpRequest;
52+
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
53+
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
54+
import software.amazon.awssdk.utils.AttributeMap;
55+
56+
@RunWith(MockitoJUnitRunner.class)
57+
public class NettyNioAsyncHttpClientNonBlockingDnsTest {
58+
59+
private final RecordingNetworkTrafficListener wiremockTrafficListener = new RecordingNetworkTrafficListener();
60+
61+
private static final SdkAsyncHttpClient client = NettyNioAsyncHttpClient.builder()
62+
.buildWithDefaults(
63+
AttributeMap.builder()
64+
.put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true)
65+
.put(SdkHttpConfigurationOption.USE_NONBLOCKING_DNS_RESOLVER, true)
66+
.build());
67+
68+
@Rule
69+
public WireMockRule mockServer = new WireMockRule(wireMockConfig()
70+
.dynamicPort()
71+
.dynamicHttpsPort()
72+
.networkTrafficListener(wiremockTrafficListener));
73+
74+
@Before
75+
public void methodSetup() {
76+
wiremockTrafficListener.reset();
77+
}
78+
79+
@AfterClass
80+
public static void tearDown() throws Exception {
81+
client.close();
82+
}
83+
84+
@Test
85+
public void useNonBlockingDnsResolver_shouldHonor() {
86+
try (NettyNioAsyncHttpClient client = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder()
87+
.build()) {
88+
assertThat(client.configuration().isNonBlockingResolver()).isEqualTo(false);
89+
}
90+
91+
try (NettyNioAsyncHttpClient client = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder()
92+
.useNonBlockingDnsResolver(false)
93+
.build()) {
94+
assertThat(client.configuration().isNonBlockingResolver()).isEqualTo(false);
95+
}
96+
97+
try (NettyNioAsyncHttpClient client = (NettyNioAsyncHttpClient) NettyNioAsyncHttpClient.builder()
98+
.useNonBlockingDnsResolver(true)
99+
.build()) {
100+
assertThat(client.configuration().isNonBlockingResolver()).isEqualTo(true);
101+
}
102+
}
103+
104+
@Test
105+
public void canSendContentAndGetThatContentBackNonBlockingDns() throws Exception {
106+
String body = randomAlphabetic(50);
107+
stubFor(any(urlEqualTo("/echo?reversed=true"))
108+
.withRequestBody(equalTo(body))
109+
.willReturn(aResponse().withBody(reverse(body))));
110+
URI uri = URI.create("http://localhost:" + mockServer.port());
111+
112+
SdkHttpRequest request = createRequest(uri, "/echo", body, SdkHttpMethod.POST, singletonMap("reversed", "true"));
113+
114+
RecordingResponseHandler recorder = new RecordingResponseHandler();
115+
116+
client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider(body)).responseHandler(recorder).build());
117+
118+
recorder.completeFuture.get(5, TimeUnit.SECONDS);
119+
120+
verify(1, postRequestedFor(urlEqualTo("/echo?reversed=true")));
121+
122+
assertThat(recorder.fullResponseAsString()).isEqualTo(reverse(body));
123+
}
124+
125+
@Test
126+
public void defaultThreadFactoryUsesHelpfulName() throws Exception {
127+
// Make a request to ensure a thread is primed
128+
makeSimpleRequest(client, mockServer);
129+
130+
String expectedPattern = "aws-java-sdk-NettyEventLoop-\\d+-\\d+";
131+
assertThat(Thread.getAllStackTraces().keySet())
132+
.areAtLeast(1, new Condition<>(t -> t.getName().matches(expectedPattern),
133+
"Matches default thread pattern: `%s`", expectedPattern));
134+
}
135+
136+
@Test
137+
public void canMakeBasicRequestOverHttp() throws Exception {
138+
String smallBody = randomAlphabetic(10);
139+
URI uri = URI.create("http://localhost:" + mockServer.port());
140+
141+
assertCanReceiveBasicRequest(client, uri, smallBody);
142+
}
143+
144+
@Test
145+
public void canMakeBasicRequestOverHttps() throws Exception {
146+
String smallBody = randomAlphabetic(10);
147+
URI uri = URI.create("https://localhost:" + mockServer.httpsPort());
148+
149+
assertCanReceiveBasicRequest(client, uri, smallBody);
150+
}
151+
152+
@Test
153+
public void canHandleLargerPayloadsOverHttp() throws Exception {
154+
String largishBody = randomAlphabetic(25000);
155+
156+
URI uri = URI.create("http://localhost:" + mockServer.port());
157+
158+
assertCanReceiveBasicRequest(client, uri, largishBody);
159+
}
160+
161+
@Test
162+
public void canHandleLargerPayloadsOverHttps() throws Exception {
163+
String largishBody = randomAlphabetic(25000);
164+
165+
URI uri = URI.create("https://localhost:" + mockServer.httpsPort());
166+
167+
assertCanReceiveBasicRequest(client, uri, largishBody);
168+
}
169+
170+
@Test
171+
public void requestContentOnlyEqualToContentLengthHeaderFromProvider() throws InterruptedException, ExecutionException, TimeoutException, IOException {
172+
final String content = randomAlphabetic(32);
173+
final String streamContent = content + reverse(content);
174+
stubFor(any(urlEqualTo("/echo?reversed=true"))
175+
.withRequestBody(equalTo(content))
176+
.willReturn(aResponse().withBody(reverse(content))));
177+
URI uri = URI.create("http://localhost:" + mockServer.port());
178+
179+
SdkHttpFullRequest request = createRequest(uri, "/echo", streamContent, SdkHttpMethod.POST, singletonMap("reversed", "true"));
180+
request = request.toBuilder().putHeader("Content-Length", Integer.toString(content.length())).build();
181+
RecordingResponseHandler recorder = new RecordingResponseHandler();
182+
183+
client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(createProvider(streamContent)).responseHandler(recorder).build());
184+
185+
recorder.completeFuture.get(5, TimeUnit.SECONDS);
186+
187+
// HTTP servers will stop processing the request as soon as it reads
188+
// bytes equal to 'Content-Length' so we need to inspect the raw
189+
// traffic to ensure that there wasn't anything after that.
190+
assertThat(wiremockTrafficListener.requests().toString()).endsWith(content);
191+
}
192+
}

0 commit comments

Comments
 (0)