/*
* Copyright 2012 LinkedIn Corp.
*
* 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 azkaban.trigger;
import azkaban.ServiceProvider;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.PriorityBlockingQueue;
import org.apache.log4j.Logger;
import azkaban.event.Event;
import azkaban.event.EventHandler;
import azkaban.event.EventListener;
import azkaban.event.Event.Type;
import azkaban.executor.ExecutableFlow;
import azkaban.executor.ExecutorManager;
import azkaban.utils.Props;
public class TriggerManager extends EventHandler implements
TriggerManagerAdapter {
private static Logger logger = Logger.getLogger(TriggerManager.class);
public static final long DEFAULT_SCANNER_INTERVAL_MS = 60000;
private static Map<Integer, Trigger> triggerIdMap =
new ConcurrentHashMap<Integer, Trigger>();
private CheckerTypeLoader checkerTypeLoader;
private ActionTypeLoader actionTypeLoader;
private TriggerLoader triggerLoader;
private final TriggerScannerThread runnerThread;
private long lastRunnerThreadCheckTime = -1;
private long runnerThreadIdleTime = -1;
private LocalTriggerJMX jmxStats = new LocalTriggerJMX();
private ExecutorManagerEventListener listener =
new ExecutorManagerEventListener();
private final Object syncObj = new Object();
private String scannerStage = "";
// TODO kunkun-tang: Before apply guice to this class, we should make
// ExecutorManager guiceable.
public TriggerManager(Props props, TriggerLoader triggerLoader,
ExecutorManager executorManager) throws TriggerManagerException {
// TODO kunkun-tang: Doing hack here to allow calling new azkaban-db code. Should fix in future.
this.triggerLoader = ServiceProvider.SERVICE_PROVIDER.getInstance(TriggerLoader.class);
long scannerInterval =
props.getLong("trigger.scan.interval", DEFAULT_SCANNER_INTERVAL_MS);
runnerThread = new TriggerScannerThread(scannerInterval);
checkerTypeLoader = new CheckerTypeLoader();
actionTypeLoader = new ActionTypeLoader();
try {
checkerTypeLoader.init(props);
actionTypeLoader.init(props);
} catch (Exception e) {
throw new TriggerManagerException(e);
}
Condition.setCheckerLoader(checkerTypeLoader);
Trigger.setActionTypeLoader(actionTypeLoader);
executorManager.addListener(listener);
logger.info("TriggerManager loaded.");
}
@Override
public void start() throws TriggerManagerException {
try {
// expect loader to return valid triggers
List<Trigger> triggers = triggerLoader.loadTriggers();
for (Trigger t : triggers) {
runnerThread.addTrigger(t);
triggerIdMap.put(t.getTriggerId(), t);
}
} catch (Exception e) {
logger.error(e);
throw new TriggerManagerException(e);
}
runnerThread.start();
}
protected CheckerTypeLoader getCheckerLoader() {
return checkerTypeLoader;
}
protected ActionTypeLoader getActionLoader() {
return actionTypeLoader;
}
public void insertTrigger(Trigger t) throws TriggerManagerException {
logger.info("Inserting trigger " + t + " in TriggerManager");
synchronized (syncObj) {
try {
triggerLoader.addTrigger(t);
} catch (TriggerLoaderException e) {
throw new TriggerManagerException(e);
}
runnerThread.addTrigger(t);
triggerIdMap.put(t.getTriggerId(), t);
}
}
public void removeTrigger(int id) throws TriggerManagerException {
logger.info("Removing trigger with id: " + id + " from TriggerManager");
synchronized (syncObj) {
Trigger t = triggerIdMap.get(id);
if (t != null) {
removeTrigger(triggerIdMap.get(id));
}
}
}
public void updateTrigger(Trigger t) throws TriggerManagerException {
logger.info("Updating trigger " + t + " in TriggerManager");
synchronized (syncObj) {
runnerThread.deleteTrigger(triggerIdMap.get(t.getTriggerId()));
runnerThread.addTrigger(t);
triggerIdMap.put(t.getTriggerId(), t);
}
}
public void removeTrigger(Trigger t) throws TriggerManagerException {
logger.info("Removing trigger " + t + " from TriggerManager");
synchronized (syncObj) {
runnerThread.deleteTrigger(t);
triggerIdMap.remove(t.getTriggerId());
try {
t.stopCheckers();
triggerLoader.removeTrigger(t);
} catch (TriggerLoaderException e) {
throw new TriggerManagerException(e);
}
}
}
public List<Trigger> getTriggers() {
return new ArrayList<Trigger>(triggerIdMap.values());
}
public Map<String, Class<? extends ConditionChecker>> getSupportedCheckers() {
return checkerTypeLoader.getSupportedCheckers();
}
private class TriggerScannerThread extends Thread {
private BlockingQueue<Trigger> triggers;
private Map<Integer, ExecutableFlow> justFinishedFlows;
private boolean shutdown = false;
private final long scannerInterval;
public TriggerScannerThread(long scannerInterval) {
triggers = new PriorityBlockingQueue<Trigger>(1, new TriggerComparator());
justFinishedFlows = new ConcurrentHashMap<Integer, ExecutableFlow>();
this.setName("TriggerRunnerManager-Trigger-Scanner-Thread");
this.scannerInterval = scannerInterval;
}
public void shutdown() {
logger.error("Shutting down trigger manager thread " + this.getName());
shutdown = true;
this.interrupt();
}
public void addJustFinishedFlow(ExecutableFlow flow) {
synchronized (syncObj) {
justFinishedFlows.put(flow.getExecutionId(), flow);
}
}
public void addTrigger(Trigger t) {
synchronized (syncObj) {
t.updateNextCheckTime();
triggers.add(t);
}
}
public void deleteTrigger(Trigger t) {
triggers.remove(t);
}
@Override
public void run() {
while (!shutdown) {
synchronized (syncObj) {
try {
lastRunnerThreadCheckTime = System.currentTimeMillis();
scannerStage =
"Ready to start a new scan cycle at "
+ lastRunnerThreadCheckTime;
try {
checkAllTriggers();
justFinishedFlows.clear();
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
} catch (Throwable t) {
t.printStackTrace();
logger.error(t.getMessage());
}
scannerStage = "Done flipping all triggers.";
runnerThreadIdleTime =
scannerInterval
- (System.currentTimeMillis() - lastRunnerThreadCheckTime);
if (runnerThreadIdleTime < 0) {
logger.error("Trigger manager thread " + this.getName()
+ " is too busy!");
} else {
syncObj.wait(runnerThreadIdleTime);
}
} catch (InterruptedException e) {
logger.info("Interrupted. Probably to shut down.");
}
}
}
}
private void checkAllTriggers() throws TriggerManagerException {
// sweep through the rest of them
for (Trigger t : triggers) {
try {
scannerStage = "Checking for trigger " + t.getTriggerId();
if (t.getStatus().equals(TriggerStatus.READY)) {
if (t.triggerConditionMet()) {
onTriggerTrigger(t);
} else if (t.expireConditionMet()) {
onTriggerExpire(t);
}
}
if (t.getStatus().equals(TriggerStatus.EXPIRED) && t.getSource().equals("azkaban")) {
removeTrigger(t);
} else {
t.updateNextCheckTime();
}
} catch (Throwable th) {
//skip this trigger, moving on to the next one
logger.error("Failed to process trigger with id : " + t, th);
}
}
}
private void onTriggerTrigger(Trigger t) throws TriggerManagerException {
List<TriggerAction> actions = t.getTriggerActions();
for (TriggerAction action : actions) {
try {
logger.info("Doing trigger actions " + action.getDescription() + " for " + t);
action.doAction();
} catch (Exception e) {
logger.error("Failed to do action " + action.getDescription() + " for " + t, e);
} catch (Throwable th) {
logger.error("Failed to do action " + action.getDescription() + " for " + t, th);
}
}
if (t.isResetOnTrigger()) {
t.resetTriggerConditions();
t.resetExpireCondition();
} else {
t.setStatus(TriggerStatus.EXPIRED);
}
try {
triggerLoader.updateTrigger(t);
} catch (TriggerLoaderException e) {
throw new TriggerManagerException(e);
}
}
private void onTriggerExpire(Trigger t) throws TriggerManagerException {
List<TriggerAction> expireActions = t.getExpireActions();
for (TriggerAction action : expireActions) {
try {
logger.info("Doing expire actions for "+ action.getDescription() + " for " + t);
action.doAction();
} catch (Exception e) {
logger.error("Failed to do expire action " + action.getDescription() + " for " + t, e);
} catch (Throwable th) {
logger.error("Failed to do expire action " + action.getDescription() + " for " + t, th);
}
}
if (t.isResetOnExpire()) {
t.resetTriggerConditions();
t.resetExpireCondition();
} else {
t.setStatus(TriggerStatus.EXPIRED);
}
try {
triggerLoader.updateTrigger(t);
} catch (TriggerLoaderException e) {
throw new TriggerManagerException(e);
}
}
private class TriggerComparator implements Comparator<Trigger> {
@Override
public int compare(Trigger arg0, Trigger arg1) {
long first = arg1.getNextCheckTime();
long second = arg0.getNextCheckTime();
if (first == second) {
return 0;
} else if (first < second) {
return 1;
}
return -1;
}
}
}
public Trigger getTrigger(int triggerId) {
synchronized (syncObj) {
return triggerIdMap.get(triggerId);
}
}
public void expireTrigger(int triggerId) {
Trigger t = getTrigger(triggerId);
t.setStatus(TriggerStatus.EXPIRED);
}
@Override
public List<Trigger> getTriggers(String triggerSource) {
List<Trigger> triggers = new ArrayList<Trigger>();
for (Trigger t : triggerIdMap.values()) {
if (t.getSource().equals(triggerSource)) {
triggers.add(t);
}
}
return triggers;
}
@Override
public List<Trigger> getTriggerUpdates(String triggerSource,
long lastUpdateTime) throws TriggerManagerException {
List<Trigger> triggers = new ArrayList<Trigger>();
for (Trigger t : triggerIdMap.values()) {
if (t.getSource().equals(triggerSource)
&& t.getLastModifyTime() > lastUpdateTime) {
triggers.add(t);
}
}
return triggers;
}
@Override
public List<Trigger> getAllTriggerUpdates(long lastUpdateTime)
throws TriggerManagerException {
List<Trigger> triggers = new ArrayList<Trigger>();
for (Trigger t : triggerIdMap.values()) {
if (t.getLastModifyTime() > lastUpdateTime) {
triggers.add(t);
}
}
return triggers;
}
@Override
public void insertTrigger(Trigger t, String user)
throws TriggerManagerException {
insertTrigger(t);
}
@Override
public void removeTrigger(int id, String user) throws TriggerManagerException {
removeTrigger(id);
}
@Override
public void updateTrigger(Trigger t, String user)
throws TriggerManagerException {
updateTrigger(t);
}
@Override
public void shutdown() {
runnerThread.shutdown();
}
@Override
public TriggerJMX getJMX() {
return this.jmxStats;
}
private class LocalTriggerJMX implements TriggerJMX {
@Override
public long getLastRunnerThreadCheckTime() {
return lastRunnerThreadCheckTime;
}
@Override
public boolean isRunnerThreadActive() {
return runnerThread.isAlive();
}
@Override
public String getPrimaryServerHost() {
return "local";
}
@Override
public int getNumTriggers() {
return triggerIdMap.size();
}
@Override
public String getTriggerSources() {
Set<String> sources = new HashSet<String>();
for (Trigger t : triggerIdMap.values()) {
sources.add(t.getSource());
}
return sources.toString();
}
@Override
public String getTriggerIds() {
return triggerIdMap.keySet().toString();
}
@Override
public long getScannerIdleTime() {
return runnerThreadIdleTime;
}
@Override
public Map<String, Object> getAllJMXMbeans() {
return new HashMap<String, Object>();
}
@Override
public String getScannerThreadStage() {
return scannerStage;
}
}
@Override
public void registerCheckerType(String name,
Class<? extends ConditionChecker> checker) {
checkerTypeLoader.registerCheckerType(name, checker);
}
@Override
public void registerActionType(String name,
Class<? extends TriggerAction> action) {
actionTypeLoader.registerActionType(name, action);
}
private class ExecutorManagerEventListener implements EventListener {
public ExecutorManagerEventListener() {
}
@Override
public void handleEvent(Event event) {
// this needs to be fixed for perf
synchronized (syncObj) {
ExecutableFlow flow = (ExecutableFlow) event.getRunner();
if (event.getType() == Type.FLOW_FINISHED) {
logger.info("Flow finish event received. " + flow.getExecutionId());
runnerThread.addJustFinishedFlow(flow);
}
}
}
}
}