Skip to content

Commit 46a9ecc

Browse files
committed
Improve exception message for high burst traffic using netty client
1 parent 90b9a42 commit 46a9ecc

File tree

5 files changed

+166
-4
lines changed

5 files changed

+166
-4
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ protected ChannelPool newPool(URI key) {
8585
FixedChannelPool.AcquireTimeoutAction.FAIL,
8686
configuration.connectionAcquisitionTimeout(),
8787
configuration.maxConnectionsPerEndpoint(),
88-
10_000);
88+
configuration.maxPendingAcquires());
8989
}
9090
};
9191
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public final class NettySdkHttpClientFactory
4646
private final Duration readTimeout;
4747
private final Duration writeTimeout;
4848
private final Duration connectionAcquisitionTimeout;
49+
private final Integer maxPendingAcquires;
4950

5051
private NettySdkHttpClientFactory(DefaultBuilder builder) {
5152
this.standardOptions = builder.standardOptions.build();
@@ -54,6 +55,7 @@ private NettySdkHttpClientFactory(DefaultBuilder builder) {
5455
this.readTimeout = validateIsWholeSecond(builder.readTimeout, "readTimeout");
5556
this.writeTimeout = validateIsWholeSecond(builder.writeTimeout, "writeTimeout");
5657
this.connectionAcquisitionTimeout = builder.connectionAcquisitionTimeout;
58+
this.maxPendingAcquires = builder.maxPendingAcquires;
5759
}
5860

5961
/**
@@ -64,6 +66,14 @@ public Optional<Integer> maxConnectionsPerEndpoint() {
6466
return Optional.ofNullable(standardOptions.get(MAX_CONNECTIONS));
6567
}
6668

69+
/**
70+
* @return Optional of the maxPendingAcquires setting.
71+
* @see Builder#maxPendingAcquires(Integer)
72+
*/
73+
public Optional<Integer> maxPendingAcquires() {
74+
return Optional.ofNullable(maxPendingAcquires);
75+
}
76+
6777
/**
6878
* @return Optional of the writeTimeout setting.
6979
* @see Builder#writeTimeout(Duration)
@@ -177,6 +187,14 @@ public interface Builder extends CopyableBuilder<Builder, NettySdkHttpClientFact
177187
*/
178188
Builder maxConnectionsPerEndpoint(Integer maxConnectionsPerEndpoint);
179189

190+
/**
191+
* The maximum number of pending acquires allowed. Once this exceeds, acquire tries will be failed.
192+
*
193+
* @param maxPendingAcquires Max number of pending acquires
194+
* @return This builder for method chaining.
195+
*/
196+
Builder maxPendingAcquires(Integer maxPendingAcquires);
197+
180198
/**
181199
* The amount of time to wait for a read on a socket before an exception is thrown.
182200
* <br/>
@@ -259,6 +277,7 @@ private static final class DefaultBuilder implements Builder {
259277
private Duration readTimeout;
260278
private Duration writeTimeout;
261279
private Duration connectionAcquisitionTimeout;
280+
private Integer maxPendingAcquires;
262281

263282
private DefaultBuilder(AttributeMap.Builder standardOptions) {
264283
this.standardOptions = standardOptions;
@@ -270,6 +289,12 @@ public Builder maxConnectionsPerEndpoint(Integer maxConnectionsPerEndpoint) {
270289
return this;
271290
}
272291

292+
@Override
293+
public Builder maxPendingAcquires(Integer maxPendingAcquires) {
294+
this.maxPendingAcquires = maxPendingAcquires;
295+
return this;
296+
}
297+
273298
@Override
274299
public Builder readTimeout(Duration timeout) {
275300
this.readTimeout = timeout;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
public final class NettyConfiguration {
3535
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(60);
3636
private static final Duration DEFAULT_WRITE_TIMEOUT = Duration.ofSeconds(60);
37+
private static final Integer MAX_PENDING_ACQUIRE = 10_000;
3738
private final AttributeMap serviceDefaults;
3839
private final NettySdkHttpClientFactory factory;
3940

@@ -57,6 +58,13 @@ public int maxConnectionsPerEndpoint() {
5758
return serviceDefaults.get(MAX_CONNECTIONS);
5859
}
5960

61+
/**
62+
* @see NettySdkHttpClientFactory.Builder#maxPendingAcquires(Integer)
63+
*/
64+
public int maxPendingAcquires() {
65+
return factory.maxPendingAcquires().orElse(MAX_PENDING_ACQUIRE);
66+
}
67+
6068
@ReviewBeforeRelease("Support disabling strict hostname verification")
6169
public <T> Optional<T> getConfigurationValue(AttributeMap.Key<T> key) {
6270
return key == USE_STRICT_HOSTNAME_VERIFICATION ? Optional.empty() : Optional.ofNullable(serviceDefaults.get(key));

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

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.netty.util.concurrent.Future;
3535
import java.net.URI;
3636
import java.nio.ByteBuffer;
37+
import java.util.concurrent.TimeoutException;
3738
import java.util.function.Supplier;
3839
import org.reactivestreams.Publisher;
3940
import org.reactivestreams.Subscriber;
@@ -111,13 +112,81 @@ private URI endpoint() {
111112
private void handleFailure(Supplier<String> msg, Throwable cause) {
112113
log.error(msg.get(), cause);
113114
runAndLogError("Exception thrown from AsyncResponseHandler",
114-
() -> context.handler().exceptionOccurred(cause));
115+
() -> context.handler().exceptionOccurred(modifyHighBurstTrafficException(cause)));
115116
if (channel != null) {
116-
runAndLogError("Unable to release channel back to the pool.",
117-
() -> closeAndRelease(channel));
117+
runAndLogError("Unable to release channel back to the pool.", () -> closeAndRelease(channel));
118118
}
119119
}
120120

121+
private Throwable modifyHighBurstTrafficException(Throwable originalCause) {
122+
String originalMessage = originalCause.getMessage();
123+
String newMessage = null;
124+
125+
if (originalCause instanceof TimeoutException &&
126+
originalMessage.contains("Acquire operation took longer")) {
127+
newMessage = getMessageForAcquireTimeoutException();
128+
129+
} else if (originalCause instanceof IllegalStateException &&
130+
originalMessage.contains("Too many outstanding acquire operations")) {
131+
newMessage = getMessageForTooManyAcquireOperationsError();
132+
133+
} else {
134+
return originalCause;
135+
}
136+
137+
return new Throwable(newMessage, originalCause);
138+
}
139+
140+
141+
private String getMessageForAcquireTimeoutException() {
142+
StringBuilder stringBuilder = new StringBuilder();
143+
144+
stringBuilder
145+
.append("Acquire operation took longer than the configured maximum time. This indicates that a request cannot get a "
146+
+ "connection from the pool within the specified maximum time. This can be due to high request rate.\n")
147+
148+
.append("Consider taking any of the following actions to mitigate the issue: increase max connections, "
149+
+ "increase acquire timeout, or slowing the request rate.\n")
150+
151+
.append("Increasing the max connections can increase client throughput (unless the network interface is already "
152+
+ "fully utilized), but can eventually start to hit operation system limitations on the number of file "
153+
+ "descriptors used by the process. If you already are fully utilizing your network interface or cannot "
154+
+ "further increase your connection count, increasing the acquire timeout gives extra time for requests to "
155+
+ "acquire a connection before timing out. If the connections doesn't free up, the subsequent requests "
156+
+ "will still timeout.\n")
157+
158+
.append("If the above mechanisms are not able to fix the issue, try smoothing out your requests so that large "
159+
+ "traffic bursts cannot overload the client, being more efficient with the number of times you need to "
160+
+ "call AWS, or by increasing the number of hosts sending requests.");
161+
162+
return stringBuilder.toString();
163+
}
164+
165+
private String getMessageForTooManyAcquireOperationsError() {
166+
StringBuilder stringBuilder = new StringBuilder();
167+
168+
stringBuilder
169+
.append("Maximum pending connection acquisitions exceeded. The request rate is too high for the client to keep up.\n")
170+
171+
.append("Consider taking any of the following actions to mitigate the issue: increase max connections, "
172+
+ "increase max pending acquire count, decrease pool lease timeout, or slowing the request rate.\n")
173+
174+
.append("Increasing the max connections can increase client throughput (unless the network interface is already "
175+
+ "fully utilized), but can eventually start to hit operation system limitations on the number of file "
176+
+ "descriptors used by the process. If you already are fully utilizing your network interface or cannot "
177+
+ "further increase your connection count, increasing the pending acquire count allows extra requests to be "
178+
+ "buffered by the client, but can cause additional request latency and higher memory usage. If your request"
179+
+ " latency or memory usage is already too high, decreasing the lease timeout will allow requests to fail "
180+
+ "more quickly, reducing the number of pending connection acquisitions, but likely won't decrease the total "
181+
+ "number of failed requests.\n")
182+
183+
.append("If the above mechanisms are not able to fix the issue, try smoothing out your requests so that large "
184+
+ "traffic bursts cannot overload the client, being more efficient with the number of times you need to call "
185+
+ "AWS, or by increasing the number of hosts sending requests.");
186+
187+
return stringBuilder.toString();
188+
}
189+
121190
private static void closeAndRelease(Channel channel) {
122191
RequestContext requestCtx = channel.attr(REQUEST_CONTEXT_KEY).get();
123192
channel.close().addListener(ignored -> requestCtx.channelPool().release(channel));

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import static org.apache.commons.lang3.StringUtils.isBlank;
3434
import static org.apache.commons.lang3.StringUtils.reverse;
3535
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3637
import static org.mockito.Mockito.atLeastOnce;
3738
import static org.mockito.Mockito.never;
3839
import static org.mockito.Mockito.spy;
@@ -43,10 +44,13 @@
4344
import io.netty.channel.nio.NioEventLoopGroup;
4445
import java.net.URI;
4546
import java.nio.ByteBuffer;
47+
import java.time.Duration;
4648
import java.util.ArrayList;
4749
import java.util.Collection;
4850
import java.util.Collections;
51+
import java.util.List;
4952
import java.util.Map;
53+
import java.util.concurrent.CompletableFuture;
5054
import java.util.concurrent.ThreadFactory;
5155
import java.util.concurrent.TimeUnit;
5256
import java.util.stream.Stream;
@@ -332,4 +336,60 @@ public Thread newThread(Runnable r) {
332336
return new Thread(r);
333337
}
334338
}
339+
340+
@Test
341+
public void testExceptionMessageChanged_WhenPendingAcquireQueueIsFull() throws Exception {
342+
String expectedErrorMsg = "Maximum pending connection acquisitions exceeded.";
343+
344+
SdkAsyncHttpClient customClient = NettySdkHttpClientFactory.builder()
345+
.maxConnectionsPerEndpoint(1)
346+
.maxPendingAcquires(1)
347+
.build()
348+
.createHttpClient();
349+
350+
List<CompletableFuture<Void>> futures = new ArrayList<>();
351+
for (int i = 0; i < 10; i++) {
352+
futures.add(makeSimpleRequestAndReturnResponseHandler(customClient).completeFuture);
353+
}
354+
355+
assertThatThrownBy(() -> {
356+
CompletableFuture.allOf(futures.stream().toArray(CompletableFuture[]::new)).join();
357+
}).hasMessageContaining(expectedErrorMsg);
358+
359+
customClient.close();
360+
}
361+
362+
363+
@Test
364+
public void testExceptionMessageChanged_WhenConnectionTimeoutErrorEncountered() throws Exception {
365+
String expectedErrorMsg = "Acquire operation took longer than the configured maximum time. This indicates that a request "
366+
+ "cannot get a connection from the pool within the specified maximum time.";
367+
368+
SdkAsyncHttpClient customClient = NettySdkHttpClientFactory.builder()
369+
.maxConnectionsPerEndpoint(1)
370+
.connectionTimeout(Duration.ofNanos(1))
371+
.build()
372+
.createHttpClient();
373+
374+
List<CompletableFuture<Void>> futures = new ArrayList<>();
375+
for (int i = 0; i < 2; i++) {
376+
futures.add(makeSimpleRequestAndReturnResponseHandler(customClient).completeFuture);
377+
}
378+
379+
assertThatThrownBy(() -> {
380+
CompletableFuture.allOf(futures.stream().toArray(CompletableFuture[]::new)).join();
381+
}).hasMessageContaining(expectedErrorMsg);
382+
383+
customClient.close();
384+
}
385+
386+
private RecordingResponseHandler makeSimpleRequestAndReturnResponseHandler(SdkAsyncHttpClient client) throws Exception {
387+
String body = randomAlphabetic(10);
388+
URI uri = URI.create("http://localhost:" + mockServer.port());
389+
stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withBody(body).withFixedDelay(1000)));
390+
SdkHttpRequest request = createRequest(uri);
391+
RecordingResponseHandler recorder = new RecordingResponseHandler();
392+
client.prepareRequest(request, requestContext, createProvider(""), recorder).run();
393+
return recorder;
394+
}
335395
}

0 commit comments

Comments
 (0)