/*
* Copyright (C) 2012 Facebook, Inc.
*
* 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.facebook.lifecycle;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.facebook.logging.Logger;
import com.facebook.logging.LoggerImpl;
/**
* Runtime.addShutdownHook() has no guarantee of order. This class
* will run hooks by stage and in the order added within stages
*/
public class ShutdownManagerImpl<T extends Enum> implements ShutdownManager<T> {
private static final Logger LOG = LoggerImpl.getLogger(ShutdownManagerImpl.class);
private final Map<T, List<Runnable>> shutdownHooksByStage = new ConcurrentHashMap<>();
private final Thread thread;
private final Object shutdownLock = new Object();
private final T[] stages;
private final T firstStage;
private final T lastStage;
private final T defaultStage;
private T currentStage;
private boolean isShutdown = false;
public ShutdownManagerImpl(Class<T> enumClazz, T defaultStage) {
this.defaultStage = defaultStage;
if (!enumClazz.isEnum()) {
throw new IllegalArgumentException(
String.format(
"%s is not an enum class", enumClazz.getName()
)
);
}
stages = enumClazz.getEnumConstants();
for (T stage : stages) {
shutdownHooksByStage.put(stage, new ArrayList<Runnable>());
}
// three values : being/end sentinel and at least one usable value
if (stages.length < 3) {
throw new IllegalArgumentException("enum class must have at least 3 values");
}
firstStage = stages[0];
currentStage = firstStage;
lastStage = stages[stages.length - 1];
thread = new Thread(
new Runnable() {
@Override
public void run() {
internalShutdown();
}
}
);
}
@Override
public boolean tryAddShutdownHook(Runnable hook) {
return tryAddShutdownHook(defaultStage, hook);
}
@Override
public boolean tryAddShutdownHook(T stage, Runnable hook) {
if (stage == firstStage || stage == lastStage) {
throw new IllegalArgumentException(
String.format("stage %s is reserved", stage)
);
}
synchronized (shutdownLock) {
//if the stage being added is our stage or earlier, we can't accept this
if (stage.compareTo(currentStage) <= 0) {
LOG.warn(
"cannot add hook for stage %s when in stage %s",
stage,
currentStage
);
return false;
}
shutdownHooksByStage.get(stage).add(hook);
return true;
}
}
@Override
public void addShutdownHook(Runnable hook) {
addShutdownHook(defaultStage, hook);
}
@Override
public void addShutdownHook(T stage, Runnable hook) {
if (!tryAddShutdownHook(stage, hook)) {
throw new IllegalStateException(
"trying to add a hook after shutdown started"
);
}
}
@Override
public void shutdown() {
if (internalShutdown()) {
// fb303.shutdown calls this, so remove the shutdown hook after we're done
Runtime.getRuntime().removeShutdownHook(thread);
}
}
@Override
public Thread getAsThread() {
return thread;
}
/**
* @return true if this executed the shutdown
*/
private boolean internalShutdown() {
synchronized (shutdownLock) {
if (isShutdown) {
LOG.info("ignoring extra shutdown call");
return false;
}
isShutdown = true;
}
LOG.info("starting service shutdown hooks");
for (T stage : stages) {
LOG.info("starting stage " + stage);
synchronized (shutdownLock) {
currentStage = stage;
}
for (Runnable hook : shutdownHooksByStage.get(stage)) {
try {
hook.run();
} catch (Throwable t) {
LOG.warn("error running hook", t);
}
}
LOG.info("ending stage " + stage);
}
LOG.info("shutdown complete");
return true;
}
}