Skip to content

Commit 9d8df50

Browse files
authored
Merge pull request #491 from yidongnan/feature/graceful-shutdown
Wait for the grpc-server to gracefully shutdown
2 parents 496971b + 46a1840 commit 9d8df50

File tree

20 files changed

+1127
-56
lines changed

20 files changed

+1127
-56
lines changed

grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717

1818
package net.devh.boot.grpc.client.channelfactory;
1919

20+
import static java.util.Comparator.comparingLong;
2021
import static java.util.Objects.requireNonNull;
22+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2123

2224
import java.time.Duration;
25+
import java.util.ArrayList;
2326
import java.util.Collections;
2427
import java.util.List;
2528
import java.util.Map;
29+
import java.util.Map.Entry;
2630
import java.util.concurrent.ConcurrentHashMap;
2731
import java.util.concurrent.CountDownLatch;
2832
import java.util.concurrent.TimeUnit;
@@ -134,7 +138,7 @@ protected ManagedChannel newManagedChannel(final String name) {
134138
final T builder = newChannelBuilder(name);
135139
configure(builder, name);
136140
final ManagedChannel channel = builder.build();
137-
final Duration timeout = properties.getChannel(name).getImmediateConnectTimeout();
141+
final Duration timeout = this.properties.getChannel(name).getImmediateConnectTimeout();
138142
if (!timeout.isZero()) {
139143
connectOnStartup(name, channel, timeout);
140144
}
@@ -260,7 +264,7 @@ protected void watchConnectivityState(final String name, final ManagedChannel ch
260264
}
261265
}
262266

263-
private void connectOnStartup(String name, ManagedChannel channel, Duration timeout) {
267+
private void connectOnStartup(final String name, final ManagedChannel channel, final Duration timeout) {
264268
log.debug("Initiating connection to channel {}", name);
265269
channel.getState(true);
266270

@@ -270,7 +274,7 @@ private void connectOnStartup(String name, ManagedChannel channel, Duration time
270274
try {
271275
log.debug("Waiting for connection to channel {}", name);
272276
connected = !readyLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
273-
} catch (InterruptedException e) {
277+
} catch (final InterruptedException e) {
274278
Thread.currentThread().interrupt();
275279
connected = false;
276280
}
@@ -280,7 +284,7 @@ private void connectOnStartup(String name, ManagedChannel channel, Duration time
280284
log.info("Successfully connected to channel {}", name);
281285
}
282286

283-
private void waitForReady(ManagedChannel channel, CountDownLatch readySignal) {
287+
private void waitForReady(final ManagedChannel channel, final CountDownLatch readySignal) {
284288
final ConnectivityState state = channel.getState(false);
285289
log.debug("Waiting for ready state. Currently in {}", state);
286290
if (state == ConnectivityState.READY) {
@@ -302,16 +306,30 @@ public synchronized void close() {
302306
return;
303307
}
304308
this.shutdown = true;
305-
for (final ManagedChannel channel : this.channels.values()) {
309+
final List<ShutdownRecord> shutdownEntries = new ArrayList<>();
310+
for (final Entry<String, ManagedChannel> entry : this.channels.entrySet()) {
311+
final ManagedChannel channel = entry.getValue();
306312
channel.shutdown();
313+
final long gracePeriod = this.properties.getChannel(entry.getKey()).getShutdownGracePeriod().toMillis();
314+
shutdownEntries.add(new ShutdownRecord(entry.getKey(), channel, gracePeriod));
307315
}
308316
try {
309-
final long waitLimit = System.currentTimeMillis() + 60_000; // wait 60 seconds at max
310-
for (final ManagedChannel channel : this.channels.values()) {
311-
int i = 0;
312-
do {
313-
log.debug("Awaiting channel shutdown: {} ({}s)", channel, i++);
314-
} while (System.currentTimeMillis() < waitLimit && !channel.awaitTermination(1, TimeUnit.SECONDS));
317+
final long start = System.currentTimeMillis();
318+
shutdownEntries.sort(comparingLong(ShutdownRecord::getGracePeriod));
319+
320+
for (final ShutdownRecord entry : shutdownEntries) {
321+
if (!entry.channel.isTerminated()) {
322+
log.debug("Awaiting channel termination: {}", entry.name);
323+
324+
final long waitedTime = System.currentTimeMillis() - start;
325+
final long waitTime = entry.gracePeriod - waitedTime;
326+
327+
if (waitTime > 0) {
328+
entry.channel.awaitTermination(waitTime, MILLISECONDS);
329+
}
330+
entry.channel.shutdownNow();
331+
}
332+
log.debug("Completed channel termination: {}", entry.name);
315333
}
316334
} catch (final InterruptedException e) {
317335
Thread.currentThread().interrupt();
@@ -327,7 +345,26 @@ public synchronized void close() {
327345
final int channelCount = this.channels.size();
328346
this.channels.clear();
329347
this.channelStates.clear();
330-
log.debug("GrpcCannelFactory closed (including {} channels)", channelCount);
348+
log.debug("GrpcChannelFactory closed (including {} channels)", channelCount);
349+
}
350+
351+
private static class ShutdownRecord {
352+
353+
private final String name;
354+
private final ManagedChannel channel;
355+
private final long gracePeriod;
356+
357+
public ShutdownRecord(final String name, final ManagedChannel channel, final long gracePeriod) {
358+
this.name = name;
359+
this.channel = channel;
360+
// gracePeriod < 0 => Infinite
361+
this.gracePeriod = gracePeriod < 0 ? Long.MAX_VALUE : gracePeriod;
362+
}
363+
364+
long getGracePeriod() {
365+
return this.gracePeriod;
366+
}
367+
331368
}
332369

333370
}

grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,33 @@ public void setKeepAliveWithoutCalls(final Boolean keepAliveWithoutCalls) {
263263
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
264264
}
265265

266+
// --------------------------------------------------
267+
268+
@DurationUnit(ChronoUnit.SECONDS)
269+
private Duration shutdownGracePeriod;
270+
private static final Duration DEFAULT_SHUTDOWN_GRACE_PERIOD = Duration.ofSeconds(30);
271+
272+
/**
273+
* Gets the time to wait for the channel to gracefully shutdown. If set to a negative value, the channel waits
274+
* forever. If set to {@code 0} the channel will force shutdown immediately. Defaults to {@code 30s}.
275+
*
276+
* @return The time to wait for a graceful shutdown.
277+
*/
278+
public Duration getShutdownGracePeriod() {
279+
return this.shutdownGracePeriod == null ? DEFAULT_SHUTDOWN_GRACE_PERIOD : this.shutdownGracePeriod;
280+
}
281+
282+
/**
283+
* Sets the time to wait for the channel to gracefully shutdown (completing all requests). If set to a negative
284+
* value, the channel waits forever. If set to {@code 0} the channel will force shutdown immediately. Defaults to
285+
* {@code 30s}.
286+
*
287+
* @param shutdownGracePeriod The time to wait for a graceful shutdown.
288+
*/
289+
public void setShutdownGracePeriod(final Duration shutdownGracePeriod) {
290+
this.shutdownGracePeriod = shutdownGracePeriod;
291+
}
292+
266293
// --------------------------------------------------
267294
// Message Transfer
268295
// --------------------------------------------------
@@ -432,6 +459,9 @@ public void copyDefaultsFrom(final GrpcChannelProperties config) {
432459
if (this.keepAliveWithoutCalls == null) {
433460
this.keepAliveWithoutCalls = config.keepAliveWithoutCalls;
434461
}
462+
if (this.shutdownGracePeriod == null) {
463+
this.shutdownGracePeriod = config.shutdownGracePeriod;
464+
}
435465
if (this.maxInboundMessageSize == null) {
436466
this.maxInboundMessageSize = config.maxInboundMessageSize;
437467
}

grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenUnitTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.springframework.util.unit.DataSize;
3030

3131
/**
32-
* Tests whether the property resolution using suffixes works.
32+
* Tests whether the property resolution works when using suffixes.
3333
*/
3434
@ExtendWith(SpringExtension.class)
3535
@SpringBootTest(properties = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2016-2021 Michael Zhang <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
5+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
6+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7+
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
*
9+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
10+
* Software.
11+
*
12+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
13+
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
15+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16+
*/
17+
18+
package net.devh.boot.grpc.client.config;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import java.time.Duration;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.test.context.junit.jupiter.SpringExtension;
29+
30+
/**
31+
* Tests whether the property resolution works with negative values and when using suffixes.
32+
*/
33+
@ExtendWith(SpringExtension.class)
34+
@SpringBootTest(properties = {
35+
"grpc.client.test.shutdownGracePeriod=-1ms"
36+
})
37+
class GrpcChannelPropertiesNegativeGivenUnitTest {
38+
39+
@Autowired
40+
private GrpcChannelsProperties grpcChannelsProperties;
41+
42+
@Test
43+
void test() {
44+
final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test");
45+
assertEquals(Duration.ofMillis(-1), properties.getShutdownGracePeriod());
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2016-2021 Michael Zhang <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
5+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
6+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7+
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
*
9+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
10+
* Software.
11+
*
12+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
13+
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
15+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16+
*/
17+
18+
package net.devh.boot.grpc.client.config;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import java.time.Duration;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.test.context.junit.jupiter.SpringExtension;
29+
30+
/**
31+
* Tests whether the property resolution works with negative values and without suffixes.
32+
*/
33+
@ExtendWith(SpringExtension.class)
34+
@SpringBootTest(properties = {
35+
"grpc.client.test.shutdownGracePeriod=-1"
36+
})
37+
class GrpcChannelPropertiesNegativeNoUnitTest {
38+
39+
@Autowired
40+
private GrpcChannelsProperties grpcChannelsProperties;
41+
42+
@Test
43+
void test() {
44+
final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test");
45+
assertEquals(Duration.ofSeconds(-1), properties.getShutdownGracePeriod());
46+
}
47+
48+
}

grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNoUnitTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.springframework.util.unit.DataSize;
3030

3131
/**
32-
* Tests whether the property resolution without suffixes works (backwards compatibility).
32+
* Tests whether the property resolution works without suffixes (backwards compatibility).
3333
*/
3434
@ExtendWith(SpringExtension.class)
3535
@SpringBootTest(properties = {

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ public List<GrpcServerConfigurer> defaultServerConfigurers() {
135135
@ConditionalOnMissingBean
136136
@ConditionalOnBean(GrpcServerFactory.class)
137137
@Bean
138-
public GrpcServerLifecycle grpcServerLifecycle(final GrpcServerFactory factory) {
139-
return new GrpcServerLifecycle(factory);
138+
public GrpcServerLifecycle grpcServerLifecycle(final GrpcServerFactory factory, GrpcServerProperties properties) {
139+
return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod());
140140
}
141141

142142
}

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,16 @@ public ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(
8282
* The server lifecycle bean for a shaded netty based server.
8383
*
8484
* @param factory The factory used to create the lifecycle.
85+
* @param properties The server properties to use.
8586
* @return The inter-process server lifecycle bean.
8687
*/
8788
@ConditionalOnBean(ShadedNettyGrpcServerFactory.class)
8889
@Bean
89-
public GrpcServerLifecycle shadedNettyGrpcServerLifecycle(final ShadedNettyGrpcServerFactory factory) {
90-
return new GrpcServerLifecycle(factory);
90+
public GrpcServerLifecycle shadedNettyGrpcServerLifecycle(
91+
final ShadedNettyGrpcServerFactory factory,
92+
final GrpcServerProperties properties) {
93+
94+
return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod());
9195
}
9296

9397
// Then try the normal netty server
@@ -120,12 +124,16 @@ public NettyGrpcServerFactory nettyGrpcServerFactory(
120124
* The server lifecycle bean for netty based server.
121125
*
122126
* @param factory The factory used to create the lifecycle.
127+
* @param properties The server properties to use.
123128
* @return The inter-process server lifecycle bean.
124129
*/
125130
@ConditionalOnBean(NettyGrpcServerFactory.class)
126131
@Bean
127-
public GrpcServerLifecycle nettyGrpcServerLifecycle(final NettyGrpcServerFactory factory) {
128-
return new GrpcServerLifecycle(factory);
132+
public GrpcServerLifecycle nettyGrpcServerLifecycle(
133+
final NettyGrpcServerFactory factory,
134+
final GrpcServerProperties properties) {
135+
136+
return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod());
129137
}
130138

131139
/**
@@ -153,12 +161,16 @@ public InProcessGrpcServerFactory inProcessGrpcServerFactory(
153161
* The server lifecycle bean for the in-process-server.
154162
*
155163
* @param factory The factory used to create the lifecycle.
164+
* @param properties The server properties to use.
156165
* @return The in-process server lifecycle bean.
157166
*/
158167
@ConditionalOnBean(InProcessGrpcServerFactory.class)
159168
@Bean
160-
public GrpcServerLifecycle inProcessGrpcServerLifecycle(final InProcessGrpcServerFactory factory) {
161-
return new GrpcServerLifecycle(factory);
169+
public GrpcServerLifecycle inProcessGrpcServerLifecycle(
170+
final InProcessGrpcServerFactory factory,
171+
final GrpcServerProperties properties) {
172+
173+
return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod());
162174
}
163175

164176
}

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ public class GrpcServerProperties {
9393
*/
9494
private String inProcessName;
9595

96+
/**
97+
* The time to wait for the server to gracefully shutdown (completing all requests after the server started to
98+
* shutdown). If set to a negative value, the server waits forever. If set to {@code 0} the server will force
99+
* shutdown immediately. Defaults to {@code 30s}.
100+
*
101+
* @param gracefullShutdownTimeout The time to wait for a graceful shutdown.
102+
* @return The time to wait for a graceful shutdown.
103+
*/
104+
@DurationUnit(ChronoUnit.SECONDS)
105+
private Duration shutdownGracePeriod = Duration.of(30, ChronoUnit.SECONDS);
106+
96107
/**
97108
* Setting to enable keepAlive. Default to {@code false}.
98109
*

0 commit comments

Comments
 (0)