Commit 5a9fa3c8 authored by Andy Wilkinson's avatar Andy Wilkinson

Only close context that is active

Previously, SpringApplicationShutdownHook would call close() on any
registered application context even if it wasn't active as it had
already been closed. This could lead to deadlock if the context was
closed and System.exit was called during application context refresh.

This commit updates SpringApplicationShutdownHook so that it only
calls close() on active contexts. This prevents deadlock as it avoids
trying to sychronize on the context's startupShutdownMonitor on
the shutdown hook thread while it's still held on the main thread
which called System.exit and is waiting for all of the shutdown hooks
to complete.

Fixes gh-27049
parent 05b041bc
...@@ -125,6 +125,9 @@ class SpringApplicationShutdownHook implements Runnable { ...@@ -125,6 +125,9 @@ class SpringApplicationShutdownHook implements Runnable {
* @param context the context to clean * @param context the context to clean
*/ */
private void closeAndWait(ConfigurableApplicationContext context) { private void closeAndWait(ConfigurableApplicationContext context) {
if (!context.isActive()) {
return;
}
context.close(); context.close();
try { try {
int waited = 0; int waited = 0;
......
...@@ -26,10 +26,12 @@ import java.util.concurrent.CountDownLatch; ...@@ -26,10 +26,12 @@ import java.util.concurrent.CountDownLatch;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
...@@ -91,6 +93,15 @@ class SpringApplicationShutdownHookTests { ...@@ -91,6 +93,15 @@ class SpringApplicationShutdownHookTests {
assertThat(finished).containsExactly(context, handlerAction); assertThat(finished).containsExactly(context, handlerAction);
} }
@Test
void runDueToExitDuringRefreshWhenContextHasBeenClosedDoesNotDeadlock() throws InterruptedException {
GenericApplicationContext context = new GenericApplicationContext();
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
shutdownHook.registerApplicationContext(context);
context.registerBean(CloseContextAndExit.class, context, shutdownHook);
context.refresh();
}
@Test @Test
void runWhenContextIsClosedDirectlyRunsHandlerActions() { void runWhenContextIsClosedDirectlyRunsHandlerActions() {
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook(); TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
...@@ -221,4 +232,28 @@ class SpringApplicationShutdownHookTests { ...@@ -221,4 +232,28 @@ class SpringApplicationShutdownHookTests {
} }
static class CloseContextAndExit implements InitializingBean {
private final ConfigurableApplicationContext context;
private final Runnable shutdownHook;
CloseContextAndExit(ConfigurableApplicationContext context, SpringApplicationShutdownHook shutdownHook) {
this.context = context;
this.shutdownHook = shutdownHook;
}
@Override
public void afterPropertiesSet() throws Exception {
this.context.close();
// Simulate System.exit by running the hook on a separate thread and waiting
// for it to complete
Thread thread = new Thread(this.shutdownHook);
thread.start();
thread.join(15000);
assertThat(thread.isAlive()).isFalse();
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment