Skip to content

Commit ee3bb00

Browse files
committed
Awaits EventLoopGroup Shutdown when closing Netty client
1 parent a8a49f7 commit ee3bb00

File tree

9 files changed

+83
-13
lines changed

9 files changed

+83
-13
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "Netty Nio HTTP Client",
3+
"type": "bugfix",
4+
"description": "Awaits `EventLoopGroup#shutdownGracefully` to complete when closing Netty client."
5+
}

build-tools/src/main/java/software/amazon/awssdk/buildtools/checkstyle/PluralEnumNames.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ private boolean hasPublicStaticField(DetailAST ast) {
7070
DetailAST classBody = ast.findFirstToken(TokenTypes.OBJBLOCK);
7171
DetailAST maybeVariableDefinition = classBody.getFirstChild();
7272

73+
String className = ast.findFirstToken(TokenTypes.IDENT).getText();
74+
75+
// Filter out util classes
76+
if (className.endsWith("Utils")) {
77+
return false;
78+
}
79+
7380
while (maybeVariableDefinition != null) {
7481
if (maybeVariableDefinition.getType() == TokenTypes.VARIABLE_DEF) {
7582
DetailAST modifiers = maybeVariableDefinition.findFirstToken(TokenTypes.MODIFIERS);

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.READ_TIMEOUT;
2525
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.REAP_IDLE_CONNECTIONS;
2626
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.WRITE_TIMEOUT;
27+
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_FUTURE_TIMEOUT_SECONDS;
28+
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS;
29+
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS;
2730
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
2831
import static software.amazon.awssdk.utils.FunctionalUtils.runAndLogError;
2932

@@ -40,6 +43,9 @@
4043
import java.net.URI;
4144
import java.time.Duration;
4245
import java.util.concurrent.CompletableFuture;
46+
import java.util.concurrent.ExecutionException;
47+
import java.util.concurrent.TimeUnit;
48+
import java.util.concurrent.TimeoutException;
4349
import java.util.concurrent.atomic.AtomicReference;
4450
import javax.net.ssl.SSLException;
4551
import javax.net.ssl.TrustManagerFactory;
@@ -212,7 +218,23 @@ private SdkEventLoopGroup nonManagedEventLoopGroup(SdkEventLoopGroup eventLoopGr
212218
@Override
213219
public void close() {
214220
runAndLogError(log, "Unable to close channel pools", pools::close);
215-
runAndLogError(log, "Unable to shutdown event loop", sdkEventLoopGroup.eventLoopGroup()::shutdownGracefully);
221+
runAndLogError(log, "Unable to shutdown event loop", () ->
222+
closeEventLoopUninterruptibly(sdkEventLoopGroup.eventLoopGroup()));
223+
}
224+
225+
private void closeEventLoopUninterruptibly(EventLoopGroup eventLoopGroup) throws ExecutionException {
226+
try {
227+
eventLoopGroup.shutdownGracefully(EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS,
228+
EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS,
229+
TimeUnit.SECONDS)
230+
.get(EVENTLOOP_SHUTDOWN_FUTURE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
231+
} catch (InterruptedException e) {
232+
Thread.currentThread().interrupt();
233+
throw new RuntimeException(e);
234+
} catch (TimeoutException e) {
235+
throw new RuntimeException(String.format("Shutting down Netty EventLoopGroup did not complete within %s seconds",
236+
EVENTLOOP_SHUTDOWN_FUTURE_TIMEOUT_SECONDS));
237+
}
216238
}
217239

218240
/**

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
package software.amazon.awssdk.http.nio.netty.internal;
1717

18+
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS;
19+
import static software.amazon.awssdk.http.nio.netty.internal.NettyConfiguration.EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS;
20+
1821
import io.netty.channel.Channel;
1922
import io.netty.channel.ChannelFuture;
2023
import io.netty.channel.ChannelPromise;
@@ -58,7 +61,7 @@ public boolean isShuttingDown() {
5861

5962
@Override
6063
public Future<?> shutdownGracefully() {
61-
return delegate.shutdownGracefully();
64+
return shutdownGracefully(EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS, EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
6265
}
6366

6467
@Override

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
*/
3232
@SdkInternalApi
3333
public final class NettyConfiguration {
34+
35+
public static final int EVENTLOOP_SHUTDOWN_QUIET_PERIOD_SECONDS = 2;
36+
public static final int EVENTLOOP_SHUTDOWN_TIMEOUT_SECONDS = 15;
37+
public static final int EVENTLOOP_SHUTDOWN_FUTURE_TIMEOUT_SECONDS = 16;
38+
3439
private final AttributeMap configuration;
3540

3641
public NettyConfiguration(AttributeMap configuration) {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616
package software.amazon.awssdk.http.nio.netty.internal;
1717

18+
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.SUCCEEDED_FUTURE;
19+
1820
import io.netty.channel.EventLoopGroup;
1921
import io.netty.util.concurrent.Future;
22+
import java.util.concurrent.TimeUnit;
2023
import software.amazon.awssdk.annotations.SdkInternalApi;
2124

2225
/**
@@ -31,8 +34,7 @@ public NonManagedEventLoopGroup(EventLoopGroup delegate) {
3134
}
3235

3336
@Override
34-
public Future<?> shutdownGracefully() {
35-
return null;
37+
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
38+
return SUCCEEDED_FUTURE;
3639
}
37-
3840
}

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616
package software.amazon.awssdk.http.nio.netty.internal;
1717

18+
import static software.amazon.awssdk.http.nio.netty.internal.utils.NettyUtils.SUCCEEDED_FUTURE;
19+
1820
import io.netty.channel.EventLoopGroup;
1921
import io.netty.util.concurrent.Future;
22+
import java.util.concurrent.TimeUnit;
2023
import java.util.concurrent.atomic.AtomicBoolean;
2124
import software.amazon.awssdk.annotations.SdkInternalApi;
2225
import software.amazon.awssdk.annotations.SdkTestInternalApi;
@@ -46,26 +49,38 @@ private SharedSdkEventLoopGroup() {
4649
/**
4750
* @return The default {@link SdkEventLoopGroup} that will be shared across all service clients.
4851
* This is used when the customer does not specify a custom {@link SdkEventLoopGroup} or {@link SdkEventLoopGroup.Builder}.
52+
* Each SdkEventLoopGroup returned is wrapped with a new {@link ReferenceCountingEventLoopGroup}.
4953
*/
5054
@SdkInternalApi
5155
public static synchronized SdkEventLoopGroup get() {
5256
if (sharedSdkEventLoopGroup == null) {
5357
sharedSdkEventLoopGroup = SdkEventLoopGroup.builder().build();
5458
}
59+
5560
referenceCount++;
5661
return SdkEventLoopGroup.create(new ReferenceCountingEventLoopGroup(sharedSdkEventLoopGroup.eventLoopGroup()),
5762
sharedSdkEventLoopGroup.channelFactory());
5863
}
5964

6065
/**
6166
* Decrement the reference count and close if necessary.
67+
*
68+
* @param quietPeriod the quite period to use
69+
* @param timeout the timeout to use
70+
* @param unit the time unit
71+
*
72+
* @return the close future. If the shared event loop group is still being used, return a completed close future,
73+
* otherwise return the future from {@link EventLoopGroup#shutdownGracefully(long, long, TimeUnit)};
6274
*/
63-
private static synchronized void decrementReference() {
75+
private static synchronized Future<?> decrementReference(long quietPeriod, long timeout, TimeUnit unit) {
6476
referenceCount--;
6577
if (referenceCount == 0) {
66-
sharedSdkEventLoopGroup.eventLoopGroup().shutdownGracefully();
78+
Future<?> shutdownGracefully =
79+
sharedSdkEventLoopGroup.eventLoopGroup().shutdownGracefully(quietPeriod, timeout, unit);
6780
sharedSdkEventLoopGroup = null;
81+
return shutdownGracefully;
6882
}
83+
return SUCCEEDED_FUTURE;
6984
}
7085

7186
@SdkTestInternalApi
@@ -86,13 +101,13 @@ private ReferenceCountingEventLoopGroup(EventLoopGroup delegate) {
86101
}
87102

88103
@Override
89-
public Future<?> shutdownGracefully() {
104+
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
90105
// Only want to decrement the reference the first time it's closed. Shutdown is idempotent and may be
91106
// called multiple times.
92107
if (hasBeenClosed.compareAndSet(false, true)) {
93-
decrementReference();
108+
return decrementReference(quietPeriod, timeout, unit);
94109
}
95-
return null;
110+
return SUCCEEDED_FUTURE;
96111
}
97112
}
98113
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.netty.util.concurrent.Future;
2020
import io.netty.util.concurrent.GenericFutureListener;
2121
import io.netty.util.concurrent.Promise;
22+
import io.netty.util.concurrent.SucceededFuture;
2223
import java.util.concurrent.CompletableFuture;
2324
import java.util.function.BiConsumer;
2425
import java.util.function.Function;
@@ -27,6 +28,11 @@
2728
@SdkInternalApi
2829
public final class NettyUtils {
2930

31+
/**
32+
* Completed succeed future.
33+
*/
34+
public static final SucceededFuture<?> SUCCEEDED_FUTURE = new SucceededFuture<>(null, null);
35+
3036
private NettyUtils() {
3137
}
3238

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,19 @@ public void customChannelFactoryIsUsed() throws Exception {
185185
ChannelFactory channelFactory = mock(ChannelFactory.class);
186186

187187
when(channelFactory.newChannel()).thenAnswer((Answer<NioSocketChannel>) invocationOnMock -> new NioSocketChannel());
188+
EventLoopGroup customEventLoopGroup = new NioEventLoopGroup();
188189

189190
SdkAsyncHttpClient customClient =
190191
NettyNioAsyncHttpClient.builder()
191-
.eventLoopGroup(SdkEventLoopGroup.create(new NioEventLoopGroup(), channelFactory))
192+
.eventLoopGroup(SdkEventLoopGroup.create(customEventLoopGroup, channelFactory))
192193
.build();
193194

194195
makeSimpleRequest(customClient);
195196
customClient.close();
196197

197198
Mockito.verify(channelFactory, atLeastOnce()).newChannel();
199+
assertThat(customEventLoopGroup.isShuttingDown()).isFalse();
200+
customEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
198201
}
199202

200203
@Test
@@ -212,10 +215,12 @@ protected ChannelPool newPool(URI key) {
212215
SdkChannelOptions channelOptions = new SdkChannelOptions();
213216
NettyConfiguration nettyConfiguration = new NettyConfiguration(AttributeMap.empty());
214217

215-
SdkAsyncHttpClient client = new NettyNioAsyncHttpClient(eventLoopGroup, sdkChannelPoolMap, channelOptions, nettyConfiguration, 1);
218+
SdkAsyncHttpClient customerClient =
219+
new NettyNioAsyncHttpClient(eventLoopGroup, sdkChannelPoolMap, channelOptions, nettyConfiguration, 1);
216220

217-
client.close();
221+
customerClient.close();
218222
assertThat(eventLoopGroup.eventLoopGroup().isShuttingDown()).isTrue();
223+
assertThat(eventLoopGroup.eventLoopGroup().isTerminated()).isTrue();
219224
assertThat(sdkChannelPoolMap).isEmpty();
220225
Mockito.verify(channelPool).close();
221226
}

0 commit comments

Comments
 (0)