org.zodiac.sdk.shutdown.hooks.ShutdownHookRegistry Maven / Gradle / Ivy
The newest version!
package org.zodiac.sdk.shutdown.hooks;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Timer;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
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.logging.Level;
import java.util.logging.Logger;
import org.zodiac.sdk.toolkit.function.state.Obj;
import org.zodiac.sdk.toolkit.function.throwing.ThrowingRunnable;
/**
* Thing you can add runnables to to be run on orderly vm shutdown (close
* connections, etc.)
*
*/
public abstract class ShutdownHookRegistry implements ShutdownHooks {
private static final Logger LOG = Logger.getLogger(ShutdownHookRegistry.class.getName());
private final ThrowingRunnable first = ThrowingRunnable.oneShot(true);
private final ThrowingRunnable middle = ThrowingRunnable.oneShot(true);
private final ThrowingRunnable last = ThrowingRunnable.oneShot(false);
/*Main needs to be re-runnable and not drop first/middle/last after they run.*/
private final ThrowingRunnable main = ThrowingRunnable.composable(true);
private final Set waitFor = Collections.synchronizedSet(
Collections.newSetFromMap(new WeakHashMap<>()));
private final AtomicBoolean registered = new AtomicBoolean();
/*
* XXX may want to make this lazy and store it in an atomic, as creating
* thread instances has a cost; on the other hand, when are we going to
* have an application that makes thousands of instances of
* ShutdownHookRegistry?
* */
private final AtomicReference shutdownThread = new AtomicReference<>();
private long executorsWait;
private final AtomicInteger count = new AtomicInteger();
private volatile boolean running;
private DeploymentMode mode = DeploymentMode.PRODUCTION;
public ShutdownHookRegistry() {
this(500);
}
public ShutdownHookRegistry(Duration wait) {
this(wait.toMillis());
}
public ShutdownHookRegistry(long wait) {
this.executorsWait = Math.max(0, wait);
main.andAlways(last);
main.andAlways(middle);
main.andAlways(first);
}
private ShutdownThread shutdownThread(boolean create) {
return shutdownThread.updateAndGet(old -> {
if (old == null && create) {
return new ShutdownThread(this);
}
return old;
});
}
protected void install() {
shutdownThread(true).register();
}
protected void deinstall() {
ShutdownThread thread = shutdownThread(false);
if (thread != null) {
thread.deregister();
}
}
void unregisterIfCurrent(ShutdownThread thread) {
shutdownThread.getAndUpdate(old -> {
if (old == thread) {
registered.set(false);
return null;
}
return old;
});
}
@Override
public void add(Runnable toRun) {
add(toRun, Phase.MIDDLE, false);
}
@Override
public ShutdownHookRegistry addFirst(Runnable toRun) {
return add(toRun, Phase.FIRST, false);
}
@Override
public ShutdownHookRegistry addLast(Runnable toRun) {
return add(toRun, Phase.LAST, false);
}
@Override
public ShutdownHookRegistry addWeak(Runnable toRun) {
return add(toRun, Phase.MIDDLE, true);
}
@Override
public ShutdownHookRegistry addFirstWeak(Runnable toRun) {
return add(toRun, Phase.FIRST, true);
}
@Override
public ShutdownHookRegistry addLastWeak(Runnable toRun) {
return add(toRun, Phase.LAST, true);
}
@Override
public ShutdownHookRegistry add(Callable> toRun) {
return add(toRun, Phase.FIRST, false);
}
@Override
public ShutdownHookRegistry addFirst(Callable> toRun) {
return add(toRun, Phase.FIRST, false);
}
@Override
public ShutdownHookRegistry addLast(Callable> toRun) {
return add(toRun, Phase.LAST, false);
}
@Override
public ShutdownHookRegistry addWeak(Callable> toRun) {
return add(toRun, Phase.MIDDLE, true);
}
@Override
public ShutdownHookRegistry addFirstWeak(Callable> toRun) {
return add(toRun, Phase.FIRST, true);
}
@Override
public ShutdownHookRegistry addLastWeak(Callable> toRun) {
return add(toRun, Phase.LAST, true);
}
@Override
public ShutdownHookRegistry add(Timer toRun) {
return add(toRun, Phase.MIDDLE, true);
}
@Override
public ShutdownHookRegistry addFirst(Timer toRun) {
return add(toRun, Phase.FIRST, true);
}
@Override
public ShutdownHookRegistry addLast(Timer toRun) {
return add(toRun, Phase.LAST, true);
}
@Override
public ShutdownHookRegistry addResource(AutoCloseable toRun) {
return add(toRun, Phase.MIDDLE, true);
}
@Override
public ShutdownHookRegistry addResourceFirst(AutoCloseable toRun) {
return add(toRun, Phase.FIRST, true);
}
@Override
public ShutdownHookRegistry addResourceLast(AutoCloseable toRun) {
return add(toRun, Phase.LAST, true);
}
@Override
public ShutdownHookRegistry add(ExecutorService toRun) {
return add(toRun, Phase.MIDDLE, true);
}
@Override
public ShutdownHookRegistry addFirst(ExecutorService toRun) {
return add(toRun, Phase.FIRST, true);
}
@Override
public ShutdownHookRegistry addLast(ExecutorService toRun) {
return add(toRun, Phase.LAST, true);
}
@Override
public ShutdownHooks addThrowing(ThrowingRunnable toRun) {
middle.andAlways(toRun);
return this;
}
@Override
public ShutdownHooks addFirstThrowing(ThrowingRunnable toRun) {
first.andAlways(toRun);
return this;
}
@Override
public ShutdownHooks addLastThrowing(ThrowingRunnable toRun) {
last.andAlways(toRun);
return this;
}
public int shutdown() {
return runShutdownHooks();
}
protected synchronized int runShutdownHooks() {
if (running) {
LOG.log(Level.WARNING, "Attempt to reenter runShutdownHooks");
return 0;
}
int result;
try {
Obj thrown = Obj.create();
result = internalRunShutdownHooks(thrown);
thrown.ifNotNull(th -> {
LOG.log(Level.WARNING, "Exceptions thrown in shutdown hooks", th);
});
} finally {
try {
// If we are running an an application thread due to explicit
// shutdown from the application
ShutdownThread thr = shutdownThread(false);
if (thr != null) {
thr.deregister();
}
} catch (IllegalStateException ex) {
// Ok, that just means we really are running in VM shutdown
}
}
return result;
}
int internalRunShutdownHooks(Obj thrown) {
running = true;
try {
int result = 0;
try {
while (count.get() > 0) {
result += count.get();
try {
main.run();
} catch (Exception | Error ex) {
thrown.apply(old -> {
if (old != null) {
old.addSuppressed(ex);
return old;
}
return ex;
});
}
}
} finally {
long interval = 50;
Timeout timeout = new Timeout(this.executorsWait);
Set unterminated = new HashSet<>(this.waitFor);
while (!waitFor.isEmpty()) {
waitFor.clear();
Set done = new HashSet<>();
while (!unterminated.isEmpty()) {
for (ExecutorService svc : unterminated) {
if (svc.isTerminated()) {
done.add(svc);
if (done.size() == unterminated.size() || timeout.isDone()) {
break;
}
continue;
}
try {
svc.awaitTermination(interval, TimeUnit.MILLISECONDS);
} catch (Exception | Error ex) {
thrown.apply(old -> {
if (old != null) {
old.addSuppressed(ex);
return old;
}
return ex;
});
}
if (svc.isTerminated()) {
done.add(svc);
}
}
if (timeout.isDone()) {
break;
}
}
unterminated.removeAll(done);
if (timeout.isDone()) {
break;
}
}
if (!unterminated.isEmpty()) {
LOG.info(() -> "Some execututors did not terminate: " + unterminated);
}
}
return result;
} finally {
running = false;
}
}
@Override
public boolean isRunningShutdownHooks() {
return running;
}
int remaining() {
return count.get();
}
protected ShutdownHookRegistry add(Object toRun, Phase phase, boolean weak) {
try {
ThrowingRunnable target;
switch (phase) {
case FIRST:
target = first;
break;
case MIDDLE:
target = middle;
break;
case LAST:
target = last;
break;
default:
throw new AssertionError(phase);
}
ThrowingRunnable toAdd;
if (weak) {
toAdd = new WeakRun(Objects.requireNonNull(toRun, "toRun"));
} else {
toAdd = new NormalRun(Objects.requireNonNull(toRun, "toRun"));
}
target.andAlways(toAdd);
count.incrementAndGet();
return this;
} finally {
if (registered.compareAndSet(false, true)) {
onFirstAdd();
}
}
}
/**
* Called when the first shutdown hook item is added; most implementations
* will want to call install()
here, but this is specifically
* not done to make it easy to create implementations within tests that will
* never, ever add themselves as a VM shutdown hook.
*/
protected void onFirstAdd() {
}
final class NormalRun implements ThrowingRunnable {
private final Object toRun;
private volatile boolean ran;
public NormalRun(Object toRun) {
this.toRun = toRun;
}
@Override
public void run() throws Exception {
if (ran) {
return;
}
ran = true;
try {
runOne(toRun);
} finally {
count.decrementAndGet();
}
}
@Override
public String toString() {
return "Hook(" + toRun + ")";
}
}
final class WeakRun implements ThrowingRunnable {
private final Reference