/** * Licensed to Cloudera, Inc. under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Cloudera, Inc. licenses this file * to you 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.cloudera.flume.master; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.cloudera.flume.master.commands.CreateLogicalNodeForm; import com.cloudera.flume.master.commands.DecommissionLogicalNodeForm; import com.cloudera.flume.master.commands.RefreshAllCommand; import com.cloudera.flume.master.commands.RefreshCommand; import com.cloudera.flume.master.commands.SetChokeLimitForm; import com.cloudera.flume.master.commands.UnconfigCommand; import com.cloudera.flume.master.commands.UnmapLogicalNodeForm; import com.cloudera.flume.master.commands.UpdateAllCommand; import com.cloudera.flume.reporter.ReportEvent; import com.cloudera.flume.reporter.Reportable; import com.google.common.base.Preconditions; /** * We want to serialize the order of configuration commands sent to the flume * configuration server. To do this we break up actions into Commands, and * submit a command to the command manager. The commands are queued into an * order and eventually they are exec'ed by the main execution thread. * * This must be thread safe. * * TODO (jon) this stores data in memory and will eventually exhaust memory. * need to add some cleanup persist old values after some threshold * (time/space). */ public class CommandManager implements Reportable { static final Logger LOG = LoggerFactory.getLogger(CommandManager.class); // queue of commands pending execution. final LinkedBlockingQueue<CommandStatus> queue = new LinkedBlockingQueue<CommandStatus>(); // map from command string to executable command. Since we don no provide a // mechanism to add more commands, this is essentially // constant, and doesn't need guarding. final Map<String, Execable> cmds = new HashMap<String, Execable>(); // This must be accessed in a thread safe way. final SortedMap<Long, CommandStatus> statuses = new TreeMap<Long, CommandStatus>(); final AtomicLong curCommandId = new AtomicLong(); static Execable noopExec = new Execable() { /** * Optional argument is a time to sleep in millis */ public void exec(String[] args) throws MasterExecException { if (args.length == 1) { long delay = Long.parseLong(args[0]); try { Thread.sleep(delay); } catch (InterruptedException e) { LOG.debug("Delay noop interrupted", e); throw new MasterExecException("Delay Noop Interrupted!", e); } } } }; ExecThread execThread; final static Object[][] cmdArrays = { { "noop", noopExec }, { "config", ConfigCommand.buildExecable() }, { "multiconfig", MultiConfigCommand.buildExecable() }, { "unconfig", UnconfigCommand.buildExecable() }, { "refresh", RefreshCommand.buildExecable() }, { "refreshAll", RefreshAllCommand.buildExecable() }, { "updateAll", UpdateAllCommand.buildExecable() }, { "save", SaveConfigCommand.buildExecable() }, { "load", LoadConfigCommand.buildExecable() }, { "spawn", CreateLogicalNodeForm.buildExecable() }, { "map", CreateLogicalNodeForm.buildExecable() }, { "decommission", DecommissionLogicalNodeForm.buildExecable() }, { "unmap", UnmapLogicalNodeForm.buildExecable() }, { "unmapAll", UnmapLogicalNodeForm.buildUnmapAllExecable() }, { "setChokeLimit", SetChokeLimitForm.buildExecable() } }; public CommandManager() { this(cmdArrays); } /* * This is for testing */ CommandManager(Object[][] cmdArray) { for (Object[] c : cmdArray) { cmds.put((String) c[0], (Execable) c[1]); } } /** * This adds a new command to the set commands that can be executed by the * CommandManager */ public void addCommand(String cmd, Execable ex) { Preconditions.checkNotNull(cmd, "Command must not be null"); Preconditions.checkNotNull(ex, "Execable must not be null"); if (cmds.containsKey(cmd)) { LOG.warn("Command '" + cmd + "' previously existed and is being overwritten"); } cmds.put(cmd, ex); } /** * Starts the exec thread (don't do this in a constructor) */ synchronized public void start() { if (execThread != null) { LOG.error("Command Manager already started, not spawning another"); return; } execThread = new ExecThread(); execThread.start(); } /** * Cleanly stops the command manager. This blocks until the execThreads are * complete. */ synchronized public void stop() { execThread.shutdown(); execThread = null; } public long submit(Command cmd) { Preconditions.checkNotNull(cmd, "No null commands allowed, use \"noop\""); LOG.info("Submitting command: " + cmd); long cmdId = curCommandId.getAndIncrement(); CommandStatus cmdStat = CommandStatus.createCommandStatus(cmdId, cmd); synchronized (this) { // want this to be atomic statuses.put(cmdId, cmdStat); queue.add(cmdStat); } return cmdId; } public boolean isSuccess(long cmdid) { CommandStatus stat = null; synchronized (this) { stat = statuses.get(cmdid); } return stat != null && stat.isSuccess(); } public boolean isFailure(long cmdid) { CommandStatus stat = null; synchronized (this) { stat = statuses.get(cmdid); } return stat != null && stat.isFailure(); } /** * This layer should eat all exceptions, including runtime exceptions. */ void handleCommand(CommandStatus cmd) { try { if (cmd == null) { return; // do nothing } cmd.toExecing(""); exec(cmd.cmd); // if no exception, assumed to be successful cmd.toSucceeded(""); } catch (MasterExecException e) { // Log and leave info about expected exceptions LOG.warn("During " + cmd + " : " + e.getMessage()); cmd.toFailed(e.getMessage()); } catch (Exception e) { // catches runtime and unexpected validation / illegal / preconditions // exceptions. LOG.error("Unexpected exception during " + cmd + " : " + e.getMessage(), e); cmd.toFailed(e.getMessage()); } } class ExecThread extends Thread { volatile boolean done = false; CountDownLatch stopped = new CountDownLatch(1); ExecThread() { super("exec-thread"); } @Override public void run() { try { while (!done) { // only have to worry about interrupted exns here. CommandStatus cmd = queue.poll(1000, TimeUnit.MILLISECONDS); handleCommand(cmd); } } catch (InterruptedException e) { LOG.error("Master exec thread interrupted!", e); } finally { stopped.countDown(); } } public void shutdown() { done = true; try { stopped.await(); } catch (InterruptedException e) { LOG.error("Shutdown of command manager was interrupted"); } } } // package visible for testing. void exec(Command cmd) throws MasterExecException { Execable ex = cmds.get(cmd.getCommand()); if (ex == null) { throw new MasterExecException("Don't know how to handle Command: '" + cmd + "'", null); } LOG.info("Executing command: " + cmd); try { ex.exec(cmd.getArgs()); } catch (MasterExecException e) { throw e; // just rethrow } catch (IOException e) { throw new MasterExecException(e.getMessage(), e); } // other exceptions get handled at next layer. } @Override public String getName() { return "command manager"; } /* * Return the Command status for the given submission id. If the id is not * present, it returns null. */ public CommandStatus getStatus(long id) { return statuses.get(id); } // TODO (jon) convert to a regular report @Override public ReportEvent getReport() { StringBuilder html = new StringBuilder(); html.append("<div class=\"CommandManager\">"); html .append("<h2>Command history </h2>\n<table border=\"1\"><tr><th>id</th><th>State</th><th>command line</th><th>message</th></tr>\n"); List<CommandStatus> values = null; synchronized (this) { values = new ArrayList<CommandStatus>(statuses.values()); } for (CommandStatus stat : values) { html.append(" <tr>"); html.append(" <td>"); html.append(stat.getCmdID()); html.append("</td>\n"); html.append(" <td>"); html.append(stat.getState()); html.append("</td>\n"); html.append(" <td>"); html.append(stat.getCommand()); html.append("</td>\n"); html.append(" <td>"); html.append(stat.getMessage()); html.append("</td>\n"); html.append(" </tr>\n"); } html.append("</table>\n\n"); html.append("</div>"); return ReportEvent.createLegacyHtmlReport("", html.toString()); } }