Skip to content

Commit a36ad67

Browse files
committed
xds: Fix shutdownNow() becoming a no-op after shutdown()`
This fixes `XdsServerWrapper.shutdownNow()` silently doing nothing once `shutdown()` had already been called, permanently hanging threads blocked in `start()`. Previously they shared a single guard flag, meaning whichever was called first made the other a complete no-op. The fix gives `shutdownNow()`'s forceful-only work its own independent guard, following the same two-guard pattern already used by `ManagedChannelImpl`/`ServerImpl` in grpc-java core, so it always runs exactly once regardless of call order. Includes a regression test that reproduces the hang against the old code and passes against the fix. Signed-off-by: Martin Baillie <martin@baillie.id>
1 parent d51ec50 commit a36ad67

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

xds/src/main/java/io/grpc/xds/XdsServerWrapper.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ public void uncaughtException(Thread t, Throwable e) {
118118
private final CountDownLatch internalTerminationLatch = new CountDownLatch(1);
119119
private final SettableFuture<Exception> initialStartFuture = SettableFuture.create();
120120
private boolean initialStarted;
121+
// Must be accessed in syncContext.
122+
// Guards the forceful-shutdown work in shutdownNow(), independently of the shutdown AtomicBoolean
123+
// above, so it isn't skipped when shutdown()
124+
private boolean shutdownNowed;
121125
private ScheduledHandle restartTimer;
122126
private ObjectPool<XdsClient> xdsClientPool;
123127
private XdsClient xdsClient;
@@ -408,16 +412,15 @@ public void run() {
408412

409413
@Override
410414
public Server shutdownNow() {
411-
if (!shutdown.compareAndSet(false, true)) {
412-
return this;
413-
}
415+
shutdown();
414416
syncContext.execute(new Runnable() {
415417
@Override
416418
public void run() {
417-
if (!delegate.isShutdown()) {
418-
delegate.shutdownNow();
419+
if (shutdownNowed) {
420+
return;
419421
}
420-
internalShutdown();
422+
shutdownNowed = true;
423+
delegate.shutdownNow();
421424
initialStartFuture.set(new IOException("server is forcefully shut down"));
422425
}
423426
});

xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,62 @@ public void run() {
487487
}
488488
}
489489

490+
@Test
491+
public void shutdownNow_afterShutdown_stillUnblocksStartThread() throws Exception {
492+
final SettableFuture<Server> start = SettableFuture.create();
493+
Executors.newSingleThreadExecutor()
494+
.execute(
495+
new Runnable() {
496+
@Override
497+
public void run() {
498+
try {
499+
start.set(xdsServerWrapper.start());
500+
} catch (Exception ex) {
501+
start.setException(ex);
502+
}
503+
}
504+
});
505+
assertThat(xdsClient.ldsResource.get(5, TimeUnit.SECONDS))
506+
.isEqualTo("grpc/server?udpa.resource.listening_address=0.0.0.0:1");
507+
xdsServerWrapper.shutdown();
508+
xdsServerWrapper.shutdownNow();
509+
try {
510+
start.get(5, TimeUnit.SECONDS);
511+
fail("should have thrown but not");
512+
} catch (ExecutionException ex) {
513+
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
514+
assertThat(ex).hasCauseThat().hasMessageThat().isEqualTo("server is forcefully shut down");
515+
}
516+
}
517+
518+
@Test
519+
public void shutdownNow_calledTwice_forcefullyShutsDownDelegateOnce() throws Exception {
520+
final SettableFuture<Server> start = SettableFuture.create();
521+
Executors.newSingleThreadExecutor()
522+
.execute(
523+
new Runnable() {
524+
@Override
525+
public void run() {
526+
try {
527+
start.set(xdsServerWrapper.start());
528+
} catch (Exception ex) {
529+
start.setException(ex);
530+
}
531+
}
532+
});
533+
assertThat(xdsClient.ldsResource.get(5, TimeUnit.SECONDS))
534+
.isEqualTo("grpc/server?udpa.resource.listening_address=0.0.0.0:1");
535+
xdsServerWrapper.shutdownNow();
536+
xdsServerWrapper.shutdownNow();
537+
try {
538+
start.get(5, TimeUnit.SECONDS);
539+
fail("should have thrown but not");
540+
} catch (ExecutionException ex) {
541+
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
542+
}
543+
verify(mockServer, times(1)).shutdownNow();
544+
}
545+
490546
@Test
491547
public void initialStartIoException() throws Exception {
492548
final SettableFuture<Server> start = SettableFuture.create();

0 commit comments

Comments
 (0)