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;
		}
	}
}