Skip to content

Commit 00114f9

Browse files
committed
Deregister failed contexts from SpringApplicationShutdownHook
Prior to this change, SpringApplication would register contexts to SpringApplicationShutdownHook and only deregister them when they're properly closed. A failed refresh attempt does not deregister the context from the shutdown hook. When a test suite runs lots of tests failing because of failed contexts, this can build up and consume lots of resources. This commit fixes this leak and deregisters failed contexts. Fixes gh-29874
1 parent c676b8b commit 00114f9

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ private void handleRunFailure(ConfigurableApplicationContext context, Throwable
821821
reportFailure(getExceptionReporters(context), exception);
822822
if (context != null) {
823823
context.close();
824+
shutdownHook.deregisterFailedApplicationContext(context);
824825
}
825826
}
826827
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
*
4444
* @author Andy Wilkinson
4545
* @author Phillip Webb
46+
* @author Brian Clozel
4647
*/
4748
class SpringApplicationShutdownHook implements Runnable {
4849

@@ -92,6 +93,17 @@ void addRuntimeShutdownHook() {
9293
}
9394
}
9495

96+
void deregisterFailedApplicationContext(ConfigurableApplicationContext applicationContext) {
97+
synchronized (SpringApplicationShutdownHook.class) {
98+
if (!applicationContext.isActive()) {
99+
SpringApplicationShutdownHook.this.contexts.remove(applicationContext);
100+
}
101+
else {
102+
throw new IllegalStateException("Cannot unregister active application context");
103+
}
104+
}
105+
}
106+
95107
@Override
96108
public void run() {
97109
Set<ConfigurableApplicationContext> contexts;

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.awaitility.Awaitility;
2727
import org.junit.jupiter.api.Test;
2828

29+
import org.springframework.beans.factory.BeanCreationException;
2930
import org.springframework.beans.factory.InitializingBean;
3031
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3132
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
@@ -36,12 +37,14 @@
3637
import static org.assertj.core.api.Assertions.assertThat;
3738
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3839
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
40+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3941

4042
/**
4143
* Tests for {@link SpringApplicationShutdownHook}.
4244
*
4345
* @author Phillip Webb
4446
* @author Andy Wilkinson
47+
* @author Brian Clozel
4548
*/
4649
class SpringApplicationShutdownHookTests {
4750

@@ -154,6 +157,29 @@ void removeHandlerActionWhenShuttingDownThrowsException() {
154157
.withMessage("Shutdown in progress");
155158
}
156159

160+
@Test
161+
void failsWhenDeregisterActiveContext() {
162+
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
163+
ConfigurableApplicationContext context = new GenericApplicationContext();
164+
shutdownHook.registerApplicationContext(context);
165+
context.refresh();
166+
assertThatThrownBy(() -> shutdownHook.deregisterFailedApplicationContext(context))
167+
.isInstanceOf(IllegalStateException.class);
168+
assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue();
169+
}
170+
171+
@Test
172+
void deregistersFailedContext() {
173+
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
174+
GenericApplicationContext context = new GenericApplicationContext();
175+
shutdownHook.registerApplicationContext(context);
176+
context.registerBean(FailingBean.class);
177+
assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class);
178+
assertThat(shutdownHook.isApplicationContextRegistered(context)).isTrue();
179+
shutdownHook.deregisterFailedApplicationContext(context);
180+
assertThat(shutdownHook.isApplicationContextRegistered(context)).isFalse();
181+
}
182+
157183
static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook {
158184

159185
private boolean runtimeShutdownHookAdded;
@@ -259,4 +285,13 @@ public void afterPropertiesSet() throws Exception {
259285

260286
}
261287

288+
static class FailingBean implements InitializingBean {
289+
290+
@Override
291+
public void afterPropertiesSet() throws Exception {
292+
throw new IllegalArgumentException("test failure");
293+
}
294+
295+
}
296+
262297
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.boot;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.Iterator;
2223
import java.util.LinkedHashSet;
24+
import java.util.List;
2325
import java.util.Map;
2426
import java.util.Set;
2527
import java.util.concurrent.atomic.AtomicInteger;
@@ -145,6 +147,7 @@
145147
* @author Artsiom Yudovin
146148
* @author Marten Deinum
147149
* @author Nguyen Bao Sach
150+
* @author Brian Clozel
148151
*/
149152
@ExtendWith(OutputCaptureExtension.class)
150153
class SpringApplicationTests {
@@ -1285,6 +1288,20 @@ void bindsEnvironmentPrefixToSpringApplication() {
12851288
assertThat(application.getEnvironmentPrefix()).isEqualTo("my");
12861289
}
12871290

1291+
@Test
1292+
void deregistersShutdownHookForFailedApplicationContext() {
1293+
SpringApplication application = new SpringApplication(BrokenPostConstructConfig.class);
1294+
List<ApplicationEvent> events = new ArrayList<>();
1295+
application.addListeners(events::add);
1296+
application.setWebApplicationType(WebApplicationType.NONE);
1297+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run);
1298+
assertThat(events).hasAtLeastOneElementOfType(ApplicationFailedEvent.class);
1299+
ApplicationFailedEvent failure = events.stream().filter((event) -> event instanceof ApplicationFailedEvent)
1300+
.map(ApplicationFailedEvent.class::cast).findFirst().get();
1301+
assertThat(SpringApplicationShutdownHookInstance.get())
1302+
.didNotRegisterApplicationContext(failure.getApplicationContext());
1303+
}
1304+
12881305
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
12891306
S state) {
12901307
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)

0 commit comments

Comments
 (0)