/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.addthis.basis.jvm; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import com.google.common.annotations.Beta; import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sun.misc.SharedSecrets; /** * Utility methods related to JVM shutdown. The primary features are an exit method that won't cause JVM hangs, and * a utility for enforcing semantics for "if this object is made, it _must_ be closed" (or similar). There are a couple * extras as well, and I have a few fairly good ideas for the future: a try-with-resources type object that ensures * the JVM won't effectively halt until released might be easier than the function-passing method, and generalization * such that it isn't necessarily tied to the actual Runtime (and reliant on all static methods). However, this more * than suffices for now. */ @Beta public final class Shutdown { private static final Logger log = LoggerFactory.getLogger(Shutdown.class); private static final AtomicBoolean CALLED_EXIT_SELF = new AtomicBoolean(false); private static final AtomicInteger EXIT_HOLDER = new AtomicInteger(0); private static final AtomicReference<Thread> EXIT_THREAD = new AtomicReference<>(null); private static final boolean HOOK_ADDED; private static final boolean SENTINAL_ADDED; static { boolean hookAdded = false; try { SharedSecrets.getJavaLangAccess().registerShutdownHook(3, true, () -> { if (EXIT_HOLDER.get() != 0) { Runtime.getRuntime().halt(EXIT_HOLDER.get()); } }); hookAdded = true; } catch (Throwable t) { log.error("Could not register super-shutdown hook deluxe", t); } HOOK_ADDED = hookAdded; if (!HOOK_ADDED) { Thread sentinelHook = new Thread(() -> { log.debug("Shutdown has already started, our secret-weapon hook did not work, and we don't know who " + "started the shutdown or what exit code they will try to use! Therefore, we will make this " + "shutdown hook thread block another 2 seconds and then halt the jvm ourselves."); Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS); Runtime.getRuntime().halt(EXIT_HOLDER.get()); }, "JVM Shutdown Sentinel") { @Override public synchronized void start() { Thread invokingThread = Thread.currentThread(); // if the thread calling "exit" logic does not seem to be ours, then we cannot trust its exit value if (!invokingThread.equals(EXIT_THREAD.get()) && (EXIT_HOLDER.get() != 0)) { super.start(); } } }; SENTINAL_ADDED = tryAddShutdownHook(sentinelHook); } else { SENTINAL_ADDED = false; } } public static void exit(int exitCode) { boolean exitBecameNonZero; if (exitCode != 0) { log.warn("Calling exit with non-zero exit code: {}", exitCode); exitBecameNonZero = EXIT_HOLDER.compareAndSet(0, exitCode); } else { exitBecameNonZero = false; } int prevExitCode; if (exitBecameNonZero) { prevExitCode = 0; } else { prevExitCode = EXIT_HOLDER.get(); } if (!CALLED_EXIT_SELF.compareAndSet(false, true)) { if (exitBecameNonZero && HOOK_ADDED) { log.warn("Changing exit code to: {}", exitCode); } else if (exitBecameNonZero) { log.error("Previously, we already started shutdown with a normal exit code! Furthermore, our mega-" + "hook did not get set up right! Halting the JVM immediately to try to ensure error is seen."); Runtime.getRuntime().halt(exitCode); } else { log.error("Tried to call exit with exit code {}, but someone else already called with {}.", exitCode, prevExitCode); } return; } if (HOOK_ADDED || SENTINAL_ADDED) { Thread exitThread = new Thread(() -> Runtime.getRuntime().exit(exitCode), "JVM Exit Thread (code: " + exitCode + ")"); EXIT_THREAD.set(exitThread); exitThread.start(); } else if (exitCode != 0) { log.error( "Our secret weapon must have failed, _and_ either the JVM started shutting down before this class" + " was ever initialized (try to load it sooner!) or some other very bad thing has occured. In any " + "event, since this is an \"exit\" call anyway, we are just going to halt now because this is the " + "best effort attempt we can make."); Runtime.getRuntime().halt(exitCode); } } public static <T> void createWithShutdownHook(Supplier<T> creator, Consumer<T> hook) { CompletableFuture<Runnable> creationFuture = new CompletableFuture<>(); Thread shutdownHook = new Thread(() -> creationFuture.join().run(), "Shutdown Hook for " + creator); boolean hookAdded = tryAddShutdownHook(shutdownHook); if (hookAdded) { try { T created = creator.get(); creationFuture.complete(() -> hook.accept(created)); } catch (Throwable t) { if (!tryRemoveShutdownHook(shutdownHook)) { creationFuture.complete( () -> log.debug("skipping hook: {} because creator: {} failed to run normally", hook, creator)); } throw t; } } } public static boolean tryAddShutdownHook(Thread shutdownHook) { try { Runtime.getRuntime().addShutdownHook(shutdownHook); return true; } catch (IllegalStateException logged) { log.debug("adding shutdown hook failed", logged); return false; } } public static boolean tryRemoveShutdownHook(Thread shutdownHook) { try { return Runtime.getRuntime().removeShutdownHook(shutdownHook); } catch (IllegalStateException logged) { log.debug("removing shutdown hook failed", logged); return false; } } private Shutdown() {} }