SignalShutdownHandler.java
/*******************************************************************************
* Copyright (c) 2025 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
// Some portions generated by Codex
package org.eclipse.rdf4j.tools.serverboot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import sun.misc.Signal;
import sun.misc.SignalHandler;
@SuppressWarnings("restriction")
final class SignalShutdownHandler implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(SignalShutdownHandler.class);
private final AtomicBoolean triggered = new AtomicBoolean(false);
private final AtomicReference<ConfigurableApplicationContext> contextRef = new AtomicReference<>();
private final List<Registration> registrations;
static SignalShutdownHandler register(String... signalNames) {
return new SignalShutdownHandler(signalNames);
}
private SignalShutdownHandler(String... signalNames) {
List<Registration> registeredSignals = new ArrayList<>();
if (signalNames != null) {
for (String signalName : signalNames) {
if (signalName == null || signalName.isBlank()) {
continue;
}
try {
Signal signal = new Signal(signalName);
SignalHandler previous = Signal.handle(signal, sig -> handleSignal(signalName));
logger.info("Registered SIG{} handler for graceful shutdown.", signalName);
registeredSignals
.add(new Registration(signal, previous != null ? previous : SignalHandler.SIG_DFL));
} catch (IllegalArgumentException | NoClassDefFoundError | UnsupportedOperationException ex) {
logger.info("Signal {} unavailable on this platform; using JVM default. {}", signalName,
ex.toString());
}
}
}
this.registrations = Collections.unmodifiableList(registeredSignals);
}
void attachContext(ConfigurableApplicationContext context) {
contextRef.set(context);
}
private void handleSignal(String signalName) {
if (!triggered.compareAndSet(false, true)) {
return;
}
startDelayedSystemExitThread(signalName);
logger.info("SIG{} received; initiating graceful shutdown.", signalName);
ConfigurableApplicationContext context = contextRef.get();
if (context != null) {
try {
int exitCode = SpringApplication.exit(context, () -> 0);
if (context.isActive()) {
context.close();
}
logger.info("Application context closed after SIG{}, exit status {}", signalName, exitCode);
System.exit(exitCode);
} catch (Throwable e) {
logger.warn("Error while shutting down after SIG{}", signalName, e);
}
} else {
logger.warn("SIG{} received before application context became available; shutting down immediately.",
signalName);
}
}
private static void startDelayedSystemExitThread(String signalName) {
// Start a thread that will forcibly exit the JVM after a delay, in case spring-boot hangs during shutdown
Thread thread = new Thread(() -> {
try {
// Give logging a moment to flush
Thread.sleep(5 * 60 * 1000); // Forcibly exit after 5 minutes
try {
logger.error("Spring application did not exit cleanly after SIG" + signalName
+ "; forcing JVM shutdown.");
System.exit(1);
} catch (SecurityException e) {
logger.error("System.exit({}) blocked by security manager after SIG{}", 1, signalName, e);
}
} catch (InterruptedException e) {
// ignore
}
logger.info("Exiting JVM after SIG{}", signalName);
}, "SignalShutdownHandler-Exit");
thread.setDaemon(true);
thread.start();
}
@Override
public void close() {
for (Registration registration : registrations) {
Signal.handle(registration.signal, registration.previousHandler);
}
}
private static final class Registration {
private final Signal signal;
private final SignalHandler previousHandler;
private Registration(Signal signal, SignalHandler previousHandler) {
this.signal = signal;
this.previousHandler = previousHandler;
}
}
}