Check startup/shutdown thread state for close bypass in shutdown hook

See gh-31811
This commit is contained in:
Juergen Hoeller
2023-12-14 00:12:12 +01:00
parent ec0ec7a0d6
commit a612518f96

View File

@@ -205,9 +205,13 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
/** Flag that indicates whether this context has been closed already. */
private final AtomicBoolean closed = new AtomicBoolean();
/** Synchronization lock for the "refresh" and "destroy". */
/** Synchronization lock for "refresh" and "close". */
private final Lock startupShutdownLock = new ReentrantLock();
/** Currently active startup/shutdown thread. */
@Nullable
private volatile Thread startupShutdownThread;
/** Reference to the JVM shutdown hook, if registered. */
@Nullable
private Thread shutdownHook;
@@ -580,6 +584,8 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
@@ -643,6 +649,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
@@ -1022,13 +1029,16 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
if (startupShutdownLock.tryLock()) {
try {
doClose();
}
finally {
startupShutdownLock.unlock();
}
if (isStartupShutdownThreadStuck()) {
active.set(false);
return;
}
startupShutdownLock.lock();
try {
doClose();
}
finally {
startupShutdownLock.unlock();
}
}
};
@@ -1036,6 +1046,30 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
}
/**
* Determine whether an active startup/shutdown thread is currently stuck,
* e.g. through a {@code System.exit} call in a user component.
*/
private boolean isStartupShutdownThreadStuck() {
Thread activeThread = this.startupShutdownThread;
if (activeThread != null && activeThread.getState() == Thread.State.WAITING) {
// Indefinitely waiting: might be Thread.join or the like, or System.exit
activeThread.interrupt();
try {
// Leave just a little bit of time for the interruption to show effect
Thread.sleep(1);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
if (activeThread.getState() == Thread.State.WAITING) {
// Interrupted but still waiting: very likely a System.exit call
return true;
}
}
return false;
}
/**
* Close this application context, destroying all beans in its bean factory.
* <p>Delegates to {@code doClose()} for the actual closing procedure.
@@ -1045,23 +1079,31 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
*/
@Override
public void close() {
if (this.startupShutdownLock.tryLock()) {
try {
doClose();
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
if (isStartupShutdownThreadStuck()) {
this.active.set(false);
return;
}
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
doClose();
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
finally {
this.startupShutdownLock.unlock();
}
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}