Skip to content

Commit f3f119b

Browse files
wilkinsonaphilwebb
authored andcommitted
Don't shutdown logging system before contexts
Add `SpringApplicationShutdownHook` to manage orderly application shutdown, specifically around the `LoggingSystem`. `SpringApplication` now offers a `getShutdownHandlers()` method that can be used to add handlers that are guaranteed to only run after the `ApplicationContext` has been closed and is inactive. Fixes gh-26660
1 parent 39aa27e commit f3f119b

File tree

9 files changed

+606
-46
lines changed

9 files changed

+606
-46
lines changed

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot;
1818

1919
import java.lang.reflect.Constructor;
20-
import java.security.AccessControlException;
2120
import java.util.ArrayList;
2221
import java.util.Arrays;
2322
import java.util.Collection;
@@ -201,6 +200,8 @@ public class SpringApplication {
201200

202201
private static final Log logger = LogFactory.getLog(SpringApplication.class);
203202

203+
static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();
204+
204205
private Set<Class<?>> primarySources;
205206

206207
private Set<String> sources = new LinkedHashSet<>();
@@ -428,12 +429,7 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab
428429

429430
private void refreshContext(ConfigurableApplicationContext context) {
430431
if (this.registerShutdownHook) {
431-
try {
432-
context.registerShutdownHook();
433-
}
434-
catch (AccessControlException ex) {
435-
// Not allowed in some environments.
436-
}
432+
shutdownHook.registerApplicationContext(context);
437433
}
438434
refresh(context);
439435
}
@@ -987,6 +983,7 @@ public void setHeadless(boolean headless) {
987983
* registered. Defaults to {@code true} to ensure that JVM shutdowns are handled
988984
* gracefully.
989985
* @param registerShutdownHook if the shutdown hook should be registered
986+
* @see #getShutdownHandlers()
990987
*/
991988
public void setRegisterShutdownHook(boolean registerShutdownHook) {
992989
this.registerShutdownHook = registerShutdownHook;
@@ -1314,6 +1311,16 @@ public ApplicationStartup getApplicationStartup() {
13141311
return this.applicationStartup;
13151312
}
13161313

1314+
/**
1315+
* Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add
1316+
* or remove handlers that perform actions before the JVM is shutdown.
1317+
* @return a {@link SpringApplicationShutdownHandlers} instance
1318+
* @since 2.5.1
1319+
*/
1320+
public static SpringApplicationShutdownHandlers getShutdownHandlers() {
1321+
return shutdownHook.getHandlers();
1322+
}
1323+
13171324
/**
13181325
* Static helper that can be used to run a {@link SpringApplication} from the
13191326
* specified source using default settings.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot;
18+
19+
import org.springframework.context.ApplicationContext;
20+
21+
/**
22+
* Interface that can be used to add or remove code that should run when the JVM is
23+
* shutdown. Shutdown handers are similar to JVM {@link Runtime#addShutdownHook(Thread)
24+
* shutdown hooks} except that they run sequentially rather than concurrently.
25+
* <p>
26+
* Shutdown handlers are guaranteed to be called only after registered
27+
* {@link ApplicationContext} instances have been closed and are no longer active.
28+
*
29+
* @author Phillip Webb
30+
* @author Andy Wilkinson
31+
* @since 2.5.1
32+
* @see SpringApplication#getShutdownHandlers()
33+
* @see SpringApplication#setRegisterShutdownHook(boolean)
34+
*/
35+
public interface SpringApplicationShutdownHandlers {
36+
37+
/**
38+
* Add an action to the handlers that will be run when the JVM exits.
39+
* @param action the action to add
40+
*/
41+
void add(Runnable action);
42+
43+
/**
44+
* Remove a previously added an action so that it no longer runs when the JVM exits.
45+
* @param action the action to remove
46+
*/
47+
void remove(Runnable action);
48+
49+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot;
18+
19+
import java.security.AccessControlException;
20+
import java.util.Collections;
21+
import java.util.IdentityHashMap;
22+
import java.util.LinkedHashSet;
23+
import java.util.Set;
24+
import java.util.WeakHashMap;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.TimeoutException;
27+
28+
import org.apache.commons.logging.Log;
29+
import org.apache.commons.logging.LogFactory;
30+
31+
import org.springframework.context.ApplicationContext;
32+
import org.springframework.context.ApplicationListener;
33+
import org.springframework.context.ConfigurableApplicationContext;
34+
import org.springframework.context.event.ContextClosedEvent;
35+
import org.springframework.util.Assert;
36+
37+
/**
38+
* A {@link Runnable} to be used as a {@link Runtime#addShutdownHook(Thread) shutdown
39+
* hook} to perform graceful shutdown of Spring Boot applications. This hook tracks
40+
* registered application contexts as well as any actions registered via
41+
* {@link SpringApplication#getShutdownHandlers()}.
42+
*
43+
* @author Andy Wilkinson
44+
* @author Phillip Webb
45+
*/
46+
class SpringApplicationShutdownHook implements Runnable {
47+
48+
private static final int SLEEP = 50;
49+
50+
private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(10);
51+
52+
private static final Log logger = LogFactory.getLog(SpringApplicationShutdownHook.class);
53+
54+
private final Handlers handlers = new Handlers();
55+
56+
private final Set<ConfigurableApplicationContext> contexts = new LinkedHashSet<>();
57+
58+
private final Set<ConfigurableApplicationContext> closedContexts = Collections.newSetFromMap(new WeakHashMap<>());
59+
60+
private final ApplicationContextClosedListener contextCloseListener = new ApplicationContextClosedListener();
61+
62+
private boolean inProgress;
63+
64+
SpringApplicationShutdownHook() {
65+
try {
66+
addRuntimeShutdownHook();
67+
}
68+
catch (AccessControlException ex) {
69+
// Not allowed in some environments
70+
}
71+
}
72+
73+
protected void addRuntimeShutdownHook() {
74+
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
75+
}
76+
77+
SpringApplicationShutdownHandlers getHandlers() {
78+
return this.handlers;
79+
}
80+
81+
void registerApplicationContext(ConfigurableApplicationContext context) {
82+
synchronized (SpringApplicationShutdownHook.class) {
83+
assertNotInProgress();
84+
context.addApplicationListener(this.contextCloseListener);
85+
this.contexts.add(context);
86+
}
87+
}
88+
89+
@Override
90+
public void run() {
91+
Set<ConfigurableApplicationContext> contexts;
92+
Set<ConfigurableApplicationContext> closedContexts;
93+
Set<Runnable> actions;
94+
synchronized (SpringApplicationShutdownHook.class) {
95+
this.inProgress = true;
96+
contexts = new LinkedHashSet<>(this.contexts);
97+
closedContexts = new LinkedHashSet<>(this.closedContexts);
98+
actions = new LinkedHashSet<>(this.handlers.getActions());
99+
}
100+
contexts.forEach(this::closeAndWait);
101+
closedContexts.forEach(this::closeAndWait);
102+
actions.forEach(Runnable::run);
103+
}
104+
105+
boolean isApplicationContextRegistered(ConfigurableApplicationContext context) {
106+
synchronized (SpringApplicationShutdownHook.class) {
107+
return this.contexts.contains(context);
108+
}
109+
}
110+
111+
void reset() {
112+
synchronized (SpringApplicationShutdownHook.class) {
113+
this.contexts.clear();
114+
this.closedContexts.clear();
115+
this.handlers.getActions().clear();
116+
this.inProgress = false;
117+
}
118+
}
119+
120+
/**
121+
* Call {@link ConfigurableApplicationContext#close()} and wait until the context
122+
* becomes inactive. We can't assume that just because the close method returns that
123+
* the context is actually inactive. It could be that another thread is still in the
124+
* process of disposing beans.
125+
* @param context the context to clean
126+
*/
127+
private void closeAndWait(ConfigurableApplicationContext context) {
128+
context.close();
129+
try {
130+
int waited = 0;
131+
while (context.isActive()) {
132+
if (waited > TIMEOUT) {
133+
throw new TimeoutException();
134+
}
135+
Thread.sleep(SLEEP);
136+
waited += SLEEP;
137+
}
138+
}
139+
catch (InterruptedException ex) {
140+
Thread.currentThread().interrupt();
141+
logger.warn("Interrupted waiting for application context " + context + " to become inactive");
142+
}
143+
catch (TimeoutException ex) {
144+
logger.warn("Timed out waiting for application context " + context + " to become inactive", ex);
145+
}
146+
}
147+
148+
private void assertNotInProgress() {
149+
Assert.state(!SpringApplicationShutdownHook.this.inProgress, "Shutdown in progress");
150+
}
151+
152+
/**
153+
* The handler actions for this shutdown hook.
154+
*/
155+
private class Handlers implements SpringApplicationShutdownHandlers {
156+
157+
private final Set<Runnable> actions = Collections.newSetFromMap(new IdentityHashMap<>());
158+
159+
@Override
160+
public void add(Runnable action) {
161+
Assert.notNull(action, "Action must not be null");
162+
synchronized (SpringApplicationShutdownHook.class) {
163+
assertNotInProgress();
164+
this.actions.add(action);
165+
}
166+
}
167+
168+
@Override
169+
public void remove(Runnable action) {
170+
Assert.notNull(action, "Action must not be null");
171+
synchronized (SpringApplicationShutdownHook.class) {
172+
assertNotInProgress();
173+
this.actions.remove(action);
174+
}
175+
}
176+
177+
Set<Runnable> getActions() {
178+
return this.actions;
179+
}
180+
181+
}
182+
183+
/**
184+
* {@link ApplicationListener} to track closed contexts.
185+
*/
186+
private class ApplicationContextClosedListener implements ApplicationListener<ContextClosedEvent> {
187+
188+
@Override
189+
public void onApplicationEvent(ContextClosedEvent event) {
190+
// The ContextClosedEvent is fired at the start of a call to {@code close()}
191+
// and if that happens in a different thread then the context may still be
192+
// active. Rather than just removing the context, we add it to a {@code
193+
// closedContexts} set. This is weak set so that the context can be GC'd once
194+
// the {@code close()} method returns.
195+
synchronized (SpringApplicationShutdownHook.class) {
196+
ApplicationContext applicationContext = event.getApplicationContext();
197+
SpringApplicationShutdownHook.this.contexts.remove(applicationContext);
198+
SpringApplicationShutdownHook.this.closedContexts
199+
.add((ConfigurableApplicationContext) applicationContext);
200+
}
201+
}
202+
203+
}
204+
205+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/logging/LoggingApplicationListener.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,11 @@ private void onApplicationStartingEvent(ApplicationStartingEvent event) {
233233
}
234234

235235
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
236+
SpringApplication springApplication = event.getSpringApplication();
236237
if (this.loggingSystem == null) {
237-
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
238+
this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
238239
}
239-
initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
240+
initialize(event.getEnvironment(), springApplication.getClassLoader());
240241
}
241242

242243
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
@@ -398,17 +399,16 @@ private BiConsumer<String, LogLevel> getLogLevelConfigurer(LoggingSystem system)
398399
}
399400

400401
private void registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) {
401-
boolean registerShutdownHook = environment.getProperty(REGISTER_SHUTDOWN_HOOK_PROPERTY, Boolean.class, true);
402-
if (registerShutdownHook) {
402+
if (environment.getProperty(REGISTER_SHUTDOWN_HOOK_PROPERTY, Boolean.class, true)) {
403403
Runnable shutdownHandler = loggingSystem.getShutdownHandler();
404404
if (shutdownHandler != null && shutdownHookRegistered.compareAndSet(false, true)) {
405-
registerShutdownHook(new Thread(shutdownHandler));
405+
registerShutdownHook(shutdownHandler);
406406
}
407407
}
408408
}
409409

410-
void registerShutdownHook(Thread shutdownHook) {
411-
Runtime.getRuntime().addShutdownHook(shutdownHook);
410+
void registerShutdownHook(Runnable shutdownHandler) {
411+
SpringApplication.getShutdownHandlers().add(shutdownHandler);
412412
}
413413

414414
public void setOrder(int order) {

0 commit comments

Comments
 (0)