Skip to content

Commit 5a9fa3c

Browse files
committed
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
1 parent 05b041b commit 5a9fa3c

File tree

2 files changed

+38
-0
lines changed

2 files changed

+38
-0
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ void reset() {
125125
* @param context the context to clean
126126
*/
127127
private void closeAndWait(ConfigurableApplicationContext context) {
128+
if (!context.isActive()) {
129+
return;
130+
}
128131
context.close();
129132
try {
130133
int waited = 0;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import org.awaitility.Awaitility;
2727
import org.junit.jupiter.api.Test;
2828

29+
import org.springframework.beans.factory.InitializingBean;
2930
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3031
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3132
import org.springframework.context.ConfigurableApplicationContext;
3233
import org.springframework.context.support.AbstractApplicationContext;
34+
import org.springframework.context.support.GenericApplicationContext;
3335

3436
import static org.assertj.core.api.Assertions.assertThat;
3537
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -91,6 +93,15 @@ void runWhenContextIsBeingClosedInAnotherThreadWaitsUntilContextIsInactive() thr
9193
assertThat(finished).containsExactly(context, handlerAction);
9294
}
9395

96+
@Test
97+
void runDueToExitDuringRefreshWhenContextHasBeenClosedDoesNotDeadlock() throws InterruptedException {
98+
GenericApplicationContext context = new GenericApplicationContext();
99+
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
100+
shutdownHook.registerApplicationContext(context);
101+
context.registerBean(CloseContextAndExit.class, context, shutdownHook);
102+
context.refresh();
103+
}
104+
94105
@Test
95106
void runWhenContextIsClosedDirectlyRunsHandlerActions() {
96107
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
@@ -221,4 +232,28 @@ public void run() {
221232

222233
}
223234

235+
static class CloseContextAndExit implements InitializingBean {
236+
237+
private final ConfigurableApplicationContext context;
238+
239+
private final Runnable shutdownHook;
240+
241+
CloseContextAndExit(ConfigurableApplicationContext context, SpringApplicationShutdownHook shutdownHook) {
242+
this.context = context;
243+
this.shutdownHook = shutdownHook;
244+
}
245+
246+
@Override
247+
public void afterPropertiesSet() throws Exception {
248+
this.context.close();
249+
// Simulate System.exit by running the hook on a separate thread and waiting
250+
// for it to complete
251+
Thread thread = new Thread(this.shutdownHook);
252+
thread.start();
253+
thread.join(15000);
254+
assertThat(thread.isAlive()).isFalse();
255+
}
256+
257+
}
258+
224259
}

0 commit comments

Comments
 (0)