Skip to content

Commit f68a0eb

Browse files
committed
Add tests to verify connection closure and small code clean up
1 parent 89826f1 commit f68a0eb

File tree

19 files changed

+384
-22
lines changed

19 files changed

+384
-22
lines changed

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HandleResponseStage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private SdkException handleErrorResponse(SdkHttpFullResponse httpResponse,
130130
*/
131131
private void closeInputStreamIfNeeded(SdkHttpFullResponse httpResponse,
132132
boolean didRequestFail) {
133-
// Always close on failed requests. Close on successful unless streaming operation.
133+
// Always close on failed requests. Close on successful requests unless it needs connection left open
134134
if (didRequestFail || !successResponseHandler.needsConnectionLeftOpen()) {
135135
Optional.ofNullable(httpResponse)
136136
.flatMap(SdkHttpFullResponse::content) // If no content, no need to close

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,12 @@ public CompletableFuture<Response<OutputT>> prepare() {
251251
return headersFuture.thenCompose(headers -> {
252252
if (headers.isSuccessful()) {
253253
return transformFuture.thenApply(r -> Response.fromSuccess(r, response));
254-
} else {
255-
if (errorTransformFuture != null) {
256-
return errorTransformFuture.thenApply(e -> Response.fromFailure(e, response));
257-
} else {
258-
return CompletableFuture.completedFuture(Response.fromFailure(null, response));
259-
}
260254
}
255+
256+
if (errorTransformFuture != null) {
257+
return errorTransformFuture.thenApply(e -> Response.fromFailure(e, response));
258+
}
259+
return CompletableFuture.completedFuture(Response.fromFailure(null, response));
261260
});
262261
}
263262
}

core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.io.IOException;
2525
import java.net.URI;
26+
import java.util.concurrent.ExecutorService;
2627
import org.apache.log4j.BasicConfigurator;
2728
import org.junit.Assert;
2829
import org.junit.Before;
@@ -31,15 +32,16 @@
3132
import org.mockito.ArgumentCaptor;
3233
import org.mockito.Mock;
3334
import org.mockito.runners.MockitoJUnitRunner;
35+
import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption;
3436
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
3537
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
3638
import software.amazon.awssdk.core.client.config.SdkClientOption;
3739
import software.amazon.awssdk.core.exception.SdkClientException;
3840
import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient;
3941
import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils;
42+
import software.amazon.awssdk.http.ExecutableHttpRequest;
4043
import software.amazon.awssdk.http.HttpExecuteRequest;
4144
import software.amazon.awssdk.http.HttpExecuteResponse;
42-
import software.amazon.awssdk.http.ExecutableHttpRequest;
4345
import software.amazon.awssdk.http.SdkHttpClient;
4446
import software.amazon.awssdk.http.SdkHttpResponse;
4547
import utils.HttpTestUtils;
@@ -54,6 +56,9 @@ public class AmazonHttpClientTest {
5456
@Mock
5557
private ExecutableHttpRequest abortableCallable;
5658

59+
@Mock
60+
private ExecutorService executor;
61+
5762
private AmazonSyncHttpClient client;
5863

5964
@Before
@@ -146,6 +151,20 @@ public void testUserAgentPrefixAndSuffixAreAdded() {
146151
Assert.assertTrue(userAgent.endsWith(suffix));
147152
}
148153

154+
@Test
155+
public void closeClient_shouldCloseDependencies() {
156+
SdkClientConfiguration config = HttpTestUtils.testClientConfiguration()
157+
.toBuilder()
158+
.option(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR, executor)
159+
.option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient)
160+
.build();
161+
162+
AmazonSyncHttpClient client = new AmazonSyncHttpClient(config);
163+
client.close();
164+
verify(sdkHttpClient).close();
165+
verify(executor).shutdown();
166+
}
167+
149168
private void stubSuccessfulResponse() throws Exception {
150169
when(abortableCallable.call()).thenReturn(HttpExecuteResponse.builder().response(SdkHttpResponse.builder()
151170
.statusCode(200)

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
6262
import org.apache.http.protocol.HttpRequestExecutor;
6363
import software.amazon.awssdk.annotations.SdkPublicApi;
64+
import software.amazon.awssdk.annotations.SdkTestInternalApi;
6465
import software.amazon.awssdk.http.AbortableInputStream;
6566
import software.amazon.awssdk.http.ExecutableHttpRequest;
6667
import software.amazon.awssdk.http.HttpExecuteRequest;
@@ -76,6 +77,7 @@
7677
import software.amazon.awssdk.http.apache.internal.conn.SdkConnectionKeepAliveStrategy;
7778
import software.amazon.awssdk.http.apache.internal.conn.SdkTlsSocketFactory;
7879
import software.amazon.awssdk.http.apache.internal.impl.ApacheHttpRequestFactory;
80+
import software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient;
7981
import software.amazon.awssdk.http.apache.internal.impl.ConnectionManagerAwareHttpClient;
8082
import software.amazon.awssdk.http.apache.internal.utils.ApacheUtils;
8183
import software.amazon.awssdk.utils.AttributeMap;
@@ -100,6 +102,15 @@ public final class ApacheHttpClient implements SdkHttpClient {
100102
private final ApacheHttpRequestConfig requestConfig;
101103
private final AttributeMap resolvedOptions;
102104

105+
@SdkTestInternalApi
106+
ApacheHttpClient(ConnectionManagerAwareHttpClient httpClient,
107+
ApacheHttpRequestConfig requestConfig,
108+
AttributeMap resolvedOptions) {
109+
this.httpClient = httpClient;
110+
this.requestConfig = requestConfig;
111+
this.resolvedOptions = resolvedOptions;
112+
}
113+
103114
private ApacheHttpClient(DefaultBuilder builder, AttributeMap resolvedOptions) {
104115
this.httpClient = createClient(builder, resolvedOptions);
105116
this.requestConfig = createRequestConfig(builder, resolvedOptions);
@@ -136,7 +147,7 @@ private ConnectionManagerAwareHttpClient createClient(ApacheHttpClient.DefaultBu
136147
cm, connectionMaxIdleTime(configuration).toMillis());
137148
}
138149

139-
return new software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient(builder.build(), cm);
150+
return new ApacheSdkHttpClient(builder.build(), cm);
140151
}
141152

142153
private void addProxyConfig(HttpClientBuilder builder,

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/ApacheHttpRequestConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public static final class Builder {
8383
private Duration connectionTimeout;
8484
private Duration connectionAcquireTimeout;
8585
private InetAddress localAddress;
86-
private Boolean expectContinueEnabled;
86+
private boolean expectContinueEnabled;
8787
private ProxyConfiguration proxyConfiguration;
8888

8989
private Builder() {
@@ -109,7 +109,7 @@ public Builder localAddress(InetAddress localAddress) {
109109
return this;
110110
}
111111

112-
public Builder expectContinueEnabled(Boolean expectContinueEnabled) {
112+
public Builder expectContinueEnabled(boolean expectContinueEnabled) {
113113
this.expectContinueEnabled = expectContinueEnabled;
114114
return this;
115115
}

http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientWireMockTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,31 @@
1515

1616
package software.amazon.awssdk.http.apache;
1717

18+
import static org.mockito.Mockito.verify;
19+
import static org.mockito.Mockito.when;
1820
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;
1921

2022
import java.net.HttpURLConnection;
23+
import org.apache.http.conn.HttpClientConnectionManager;
2124
import org.junit.Test;
2225
import org.junit.runner.RunWith;
26+
import org.mockito.Mock;
2327
import org.mockito.runners.MockitoJUnitRunner;
2428
import software.amazon.awssdk.http.SdkHttpClient;
2529
import software.amazon.awssdk.http.SdkHttpClientTestSuite;
30+
import software.amazon.awssdk.http.apache.internal.ApacheHttpRequestConfig;
31+
import software.amazon.awssdk.http.apache.internal.impl.ConnectionManagerAwareHttpClient;
2632
import software.amazon.awssdk.utils.AttributeMap;
2733

2834
@RunWith(MockitoJUnitRunner.class)
2935
public class ApacheHttpClientWireMockTest extends SdkHttpClientTestSuite {
36+
37+
@Mock
38+
private ConnectionManagerAwareHttpClient httpClient;
39+
40+
@Mock
41+
private HttpClientConnectionManager connectionManager;
42+
3043
@Override
3144
protected SdkHttpClient createSdkHttpClient(SdkHttpClientOptions options) {
3245
return ApacheHttpClient.builder().build();
@@ -41,4 +54,14 @@ public void noSslException_WhenCertCheckingDisabled() throws Exception {
4154

4255
testForResponseCodeUsingHttps(client, HttpURLConnection.HTTP_OK);
4356
}
57+
58+
@Test
59+
public void closeClient_shouldCloseUnderlyingResources() {
60+
ApacheHttpClient client = new ApacheHttpClient(httpClient, ApacheHttpRequestConfig.builder().build(), AttributeMap.empty());
61+
when(httpClient.getHttpClientConnectionManager()).thenReturn(connectionManager);
62+
63+
client.close();
64+
verify(connectionManager).shutdown();
65+
}
66+
4467
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.slf4j.Logger;
4444
import org.slf4j.LoggerFactory;
4545
import software.amazon.awssdk.annotations.SdkPublicApi;
46+
import software.amazon.awssdk.annotations.SdkTestInternalApi;
4647
import software.amazon.awssdk.http.Protocol;
4748
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
4849
import software.amazon.awssdk.http.SdkHttpRequest;
@@ -88,6 +89,19 @@ public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient {
8889
this.sdkChannelOptions = channelOptions(builder);
8990
}
9091

92+
@SdkTestInternalApi
93+
NettyNioAsyncHttpClient(SdkEventLoopGroup sdkEventLoopGroup,
94+
SdkChannelPoolMap<URI, ChannelPool> pools,
95+
SdkChannelOptions sdkChannelOptions,
96+
NettyConfiguration configuration,
97+
long maxStreams) {
98+
this.sdkEventLoopGroup = sdkEventLoopGroup;
99+
this.pools = pools;
100+
this.sdkChannelOptions = sdkChannelOptions;
101+
this.configuration = configuration;
102+
this.maxStreams = maxStreams;
103+
}
104+
91105
private SdkChannelOptions channelOptions(DefaultBuilder builder) {
92106
return builder.sdkChannelOptions;
93107
}
@@ -171,7 +185,7 @@ private SdkEventLoopGroup nonManagedEventLoopGroup(SdkEventLoopGroup eventLoopGr
171185

172186
@Override
173187
public void close() {
174-
pools.forEach(e -> runAndLogError(log, "Unable to close channel pool for " + e.getKey(), e.getValue()::close));
188+
runAndLogError(log, "Unable to close channel pools", pools::close);
175189
runAndLogError(log, "Unable to shutdown event loop", sdkEventLoopGroup.eventLoopGroup()::shutdownGracefully);
176190
}
177191

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,17 @@ protected void channelRead0(ChannelHandlerContext channelContext, HttpObject msg
100100
ByteBuffer bb = copyToByteBuffer(fullContent);
101101
fullContent.release();
102102
requestContext.handler().onStream(new FullResponseContentPublisher(channelContext, bb, ef));
103-
finalizeRequest(requestContext, channelContext);
103+
finalizeResponse(requestContext, channelContext);
104104
}
105105
}
106106

107-
private static void finalizeRequest(RequestContext requestContext, ChannelHandlerContext channelContext) {
107+
/**
108+
* Finalize the response by completing the execute future and release the channel pool being used.
109+
*
110+
* @param requestContext the request context
111+
* @param channelContext the channel context
112+
*/
113+
private static void finalizeResponse(RequestContext requestContext, ChannelHandlerContext channelContext) {
108114
channelContext.channel().attr(RESPONSE_COMPLETE_KEY).set(true);
109115
executeFuture(channelContext).complete(null);
110116
if (!channelContext.channel().attr(KEEP_ALIVE).get()) {
@@ -268,7 +274,7 @@ public void onComplete() {
268274
runAndLogError(String.format("Subscriber %s threw an exception in onComplete.", subscriber.toString()),
269275
subscriber::onComplete);
270276
} finally {
271-
finalizeRequest(requestContext, channelContext);
277+
finalizeResponse(requestContext, channelContext);
272278
}
273279
}
274280
});

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import io.netty.channel.ChannelFuture;
4848
import io.netty.channel.EventLoopGroup;
4949
import io.netty.channel.nio.NioEventLoopGroup;
50+
import io.netty.channel.pool.ChannelPool;
5051
import io.netty.channel.socket.nio.NioSocketChannel;
5152
import java.io.IOException;
5253
import java.net.Socket;
@@ -84,6 +85,9 @@
8485
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
8586
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
8687
import software.amazon.awssdk.http.async.SdkHttpContentPublisher;
88+
import software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration;
89+
import software.amazon.awssdk.http.nio.netty.internal.SdkChannelOptions;
90+
import software.amazon.awssdk.http.nio.netty.internal.SdkChannelPoolMap;
8791
import software.amazon.awssdk.utils.AttributeMap;
8892

8993
@RunWith(MockitoJUnitRunner.class)
@@ -193,6 +197,29 @@ public void customChannelFactoryIsUsed() throws Exception {
193197
Mockito.verify(channelFactory, atLeastOnce()).newChannel();
194198
}
195199

200+
@Test
201+
public void closeClient_shouldCloseUnderlyingResources() {
202+
SdkEventLoopGroup eventLoopGroup = SdkEventLoopGroup.builder().build();
203+
ChannelPool channelPool = mock(ChannelPool.class);
204+
SdkChannelPoolMap<URI, ChannelPool> sdkChannelPoolMap = new SdkChannelPoolMap<URI, ChannelPool>() {
205+
@Override
206+
protected ChannelPool newPool(URI key) {
207+
return channelPool;
208+
}
209+
};
210+
211+
sdkChannelPoolMap.get(URI.create("http://blah"));
212+
SdkChannelOptions channelOptions = new SdkChannelOptions();
213+
NettyConfiguration nettyConfiguration = new NettyConfiguration(AttributeMap.empty());
214+
215+
SdkAsyncHttpClient client = new NettyNioAsyncHttpClient(eventLoopGroup, sdkChannelPoolMap, channelOptions, nettyConfiguration, 1);
216+
217+
client.close();
218+
assertThat(eventLoopGroup.eventLoopGroup().isShuttingDown()).isTrue();
219+
assertThat(sdkChannelPoolMap).isEmpty();
220+
Mockito.verify(channelPool).close();
221+
}
222+
196223
/**
197224
* Make a simple async request and wait for it to fiish.
198225
*

http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
9999

100100
@Override
101101
public void close() {
102-
102+
// Nothing to close. The connections will be closed by closing the InputStreams.
103103
}
104104

105105
private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest request) {

0 commit comments

Comments
 (0)