package org.apache.blur.command; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.blur.command.annotation.Description; import org.apache.blur.concurrent.Executors; import org.apache.blur.log.Log; import org.apache.blur.log.LogFactory; import org.apache.blur.thrift.generated.Arguments; import org.apache.blur.thrift.generated.BlurException; import org.apache.blur.thrift.generated.CommandStatus; import org.apache.blur.thrift.generated.CommandStatusState; import org.apache.blur.thrift.generated.User; import org.apache.commons.io.IOUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import com.google.common.collect.MapMaker; /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * 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. */ public abstract class BaseCommandManager implements Closeable { private static final String META_INF_SERVICES_ORG_APACHE_BLUR_COMMAND_COMMANDS = "META-INF/services/org.apache.blur.command.Commands"; private static final Log LOG = LogFactory.getLog(BaseCommandManager.class); private final ExecutorService _executorServiceWorker; private final ExecutorService _executorServiceDriver; private final Random _random = new Random(); protected final Map<String, BigInteger> _commandLoadTime = new ConcurrentHashMap<String, BigInteger>(); protected final Map<String, Command<?>> _command = new ConcurrentHashMap<String, Command<?>>(); protected final Map<Class<? extends Command<?>>, String> _commandNameLookup = new ConcurrentHashMap<Class<? extends Command<?>>, String>(); protected final ConcurrentMap<Long, ResponseFuture<?>> _driverRunningMap = new MapMaker().makeMap(); protected final ConcurrentMap<Long, ResponseFuture<?>> _workerRunningMap = new MapMaker().makeMap(); protected final long _connectionTimeout; protected final File _tmpPath; protected final String _commandPath; protected final Timer _timer; protected final long _pollingPeriod = TimeUnit.SECONDS.toMillis(15); protected final Map<Path, BigInteger> _commandPathLastChange = new ConcurrentHashMap<Path, BigInteger>(); protected final Configuration _configuration; protected final BlurObjectSerDe _serDe = new BlurObjectSerDe(); protected final long _runningCacheTombstoneTime = TimeUnit.SECONDS.toMillis(60); protected final String _serverName; public BaseCommandManager(File tmpPath, String commandPath, int workerThreadCount, int driverThreadCount, long connectionTimeout, Configuration configuration, String serverName) throws IOException { _serverName = serverName; _configuration = configuration; lookForCommandsToRegisterInClassPath(); _tmpPath = tmpPath; _commandPath = commandPath; _executorServiceWorker = Executors.newThreadPool("command-worker-", workerThreadCount); _executorServiceDriver = Executors.newThreadPool("command-driver-", driverThreadCount); _connectionTimeout = connectionTimeout / 2; _timer = new Timer("BaseCommandManager-Timer", true); _timer.schedule(getTimerTaskForRemovalOfOldCommands(_driverRunningMap), _pollingPeriod, _pollingPeriod); _timer.schedule(getTimerTaskForRemovalOfOldCommands(_workerRunningMap), _pollingPeriod, _pollingPeriod); if (_tmpPath == null || _commandPath == null) { LOG.info("Tmp Path [{0}] or Command Path [{1}] is null so the automatic command reload will be disabled.", _tmpPath, _commandPath); } else { loadNewCommandsFromCommandPath(); _timer.schedule(getNewCommandTimerTask(), _pollingPeriod, _pollingPeriod); } } public void cancelCommand(String commandExecutionId) { LOG.info("Trying to cancel command [{0}]", commandExecutionId); cancelAllExecuting(commandExecutionId, _workerRunningMap); cancelAllExecuting(commandExecutionId, _driverRunningMap); } private void cancelAllExecuting(String commandExecutionId, ConcurrentMap<Long, ResponseFuture<?>> runningMap) { for (Entry<Long, ResponseFuture<?>> e : runningMap.entrySet()) { Long instanceExecutionId = e.getKey(); ResponseFuture<?> future = e.getValue(); Command<?> commandExecuting = future.getCommandExecuting(); if (commandExecuting.getCommandExecutionId().equals(commandExecutionId)) { LOG.info("Canceling Command with executing id [{0}] command [{1}]", instanceExecutionId, commandExecuting); future.cancel(true); } } } public CommandStatus getCommandStatus(String commandExecutionId) { CommandStatus cso1 = findCommandStatusObject(commandExecutionId, _workerRunningMap.values()); CommandStatus cso2 = findCommandStatusObject(commandExecutionId, _driverRunningMap.values()); return CommandStatusUtil.mergeCommandStatus(cso1, cso2); } private CommandStatus findCommandStatusObject(String commandExecutionId, Collection<ResponseFuture<?>> values) { Map<String, Map<CommandStatusState, Long>> serverStateMap = new HashMap<String, Map<CommandStatusState, Long>>(); CommandStatus commandStatus = null; for (ResponseFuture<?> responseFuture : values) { if (responseFuture == null) { continue; } Command<?> commandExecuting = responseFuture.getCommandExecuting(); if (commandExecutionId.equals(commandExecuting.getCommandExecutionId())) { if (commandStatus == null) { CommandStatus originalCommandStatusObject = responseFuture.getOriginalCommandStatusObject(); String commandName = responseFuture.getCommandExecuting().getName(); Arguments arguments = originalCommandStatusObject.getArguments(); User user = originalCommandStatusObject.getUser(); commandStatus = new CommandStatus(commandExecutionId, commandName, arguments, serverStateMap, user); } CommandStatusState commandStatusStateEnum = getCommandStatusStateEnum(responseFuture); Map<CommandStatusState, Long> map = serverStateMap.get(_serverName); if (map == null) { serverStateMap.put(_serverName, map = new HashMap<CommandStatusState, Long>()); } Long l = map.get(commandStatusStateEnum); if (l == null) { map.put(commandStatusStateEnum, 1L); } else { map.put(commandStatusStateEnum, 1L + l); } } } return commandStatus; } public List<String> commandStatusList() { Set<String> result = new TreeSet<String>(); result.addAll(getStatusList(_workerRunningMap.values())); result.addAll(getStatusList(_driverRunningMap.values())); return new ArrayList<String>(result); } private List<String> getStatusList(Collection<ResponseFuture<?>> values) { List<String> result = new ArrayList<String>(); for (ResponseFuture<?> responseFuture : values) { Command<?> commandExecuting = responseFuture.getCommandExecuting(); String commandExecutionId = commandExecuting.getCommandExecutionId(); if (commandExecutionId != null) { result.add(commandExecutionId); } } return result; } private CommandStatusState getCommandStatusStateEnum(ResponseFuture<?> responseFuture) { if (responseFuture.isCancelled()) { return CommandStatusState.INTERRUPTED; } else { if (responseFuture.isDone()) { return CommandStatusState.COMPLETE; } else { return CommandStatusState.RUNNING; } } } private TimerTask getTimerTaskForRemovalOfOldCommands(final Map<Long, ResponseFuture<?>> runningMap) { return new TimerTask() { @Override public void run() { try { Set<Entry<Long, ResponseFuture<?>>> entrySet = runningMap.entrySet(); for (Entry<Long, ResponseFuture<?>> e : entrySet) { Long instanceExecutionId = e.getKey(); ResponseFuture<?> responseFuture = e.getValue(); if (!responseFuture.isRunning() && responseFuture.hasExpired()) { Command<?> commandExecuting = responseFuture.getCommandExecuting(); String commandExecutionId = null; if (commandExecuting != null) { commandExecutionId = commandExecuting.getCommandExecutionId(); } LOG.info("Removing old execution instance id [{0}] with command execution id of [{1}]", instanceExecutionId, commandExecutionId); runningMap.remove(instanceExecutionId); } } } catch (Throwable t) { LOG.error("Unknown error.", t); } } }; } public Map<String, BigInteger> getCommands() { return new HashMap<String, BigInteger>(_commandLoadTime); } public Set<Argument> getRequiredArguments(String commandName) { Command<?> command = getCommandObject(commandName, null); return command.getRequiredArguments(); } public Set<Argument> getOptionalArguments(String commandName) { Command<?> command = getCommandObject(commandName, null); return command.getOptionalArguments(); } protected TimerTask getNewCommandTimerTask() { return new TimerTask() { @Override public void run() { try { loadNewCommandsFromCommandPath(); } catch (Throwable t) { LOG.error("Unknown error while trying to load new commands.", t); } } }; } public int commandRefresh() throws IOException { return loadNewCommandsFromCommandPath(); } protected synchronized int loadNewCommandsFromCommandPath() throws IOException { Path path = new Path(_commandPath); FileSystem fileSystem = path.getFileSystem(_configuration); int changeCount = 0; if (fileSystem.exists(path)) { FileStatus[] listStatus = fileSystem.listStatus(path); for (FileStatus fileStatus : listStatus) { BigInteger contentsCheck = checkContents(fileStatus, fileSystem); Path entryPath = fileStatus.getPath(); BigInteger currentValue = _commandPathLastChange.get(entryPath); if (!contentsCheck.equals(currentValue)) { changeCount++; loadNewCommand(fileSystem, fileStatus, contentsCheck); _commandPathLastChange.put(entryPath, contentsCheck); } } } return changeCount; } protected void loadNewCommand(FileSystem fileSystem, FileStatus fileStatus, BigInteger hashOfContents) throws IOException { File file = new File(_tmpPath, UUID.randomUUID().toString()); if (!file.mkdirs()) { LOG.error("Error while trying to create a tmp directory for loading a new command set from [{0}].", fileStatus.getPath()); return; } LOG.info("Copying new command with hash [{2}] set from [{0}] into [{1}].", fileStatus.getPath(), file.getAbsolutePath(), hashOfContents.toString(Character.MAX_RADIX)); copyLocal(fileSystem, fileStatus, file); URLClassLoader loader = new URLClassLoader(getUrls(file).toArray(new URL[] {})); Enumeration<URL> resources = loader.getResources(META_INF_SERVICES_ORG_APACHE_BLUR_COMMAND_COMMANDS); loadCommandClasses(resources, loader, hashOfContents); } protected List<URL> getUrls(File file) throws MalformedURLException { List<URL> urls = new ArrayList<URL>(); if (file.isDirectory()) { for (File f : file.listFiles()) { urls.addAll(getUrls(f)); } } else { URL url = file.toURI().toURL(); LOG.info("Adding url [{0}] to be loaded.", url); urls.add(url); } return urls; } protected void copyLocal(FileSystem fileSystem, FileStatus fileStatus, File destDir) throws IOException { Path path = fileStatus.getPath(); File file = new File(destDir, path.getName()); if (fileStatus.isDirectory()) { if (!file.mkdirs()) { LOG.error("Error while trying to create a sub directory [{0}].", file.getAbsolutePath()); throw new IOException("Error while trying to create a sub directory [" + file.getAbsolutePath() + "]."); } FileStatus[] listStatus = fileSystem.listStatus(path); for (FileStatus fs : listStatus) { copyLocal(fileSystem, fs, file); } } else { FileOutputStream output = new FileOutputStream(file); FSDataInputStream inputStream = fileSystem.open(path); IOUtils.copy(inputStream, output); inputStream.close(); output.close(); } } protected BigInteger checkContents(FileStatus fileStatus, FileSystem fileSystem) throws IOException { if (fileStatus.isDirectory()) { LOG.debug("Scanning directory [{0}].", fileStatus.getPath()); BigInteger count = BigInteger.ZERO; Path path = fileStatus.getPath(); FileStatus[] listStatus = fileSystem.listStatus(path); for (FileStatus fs : listStatus) { count = count.add(checkContents(fs, fileSystem)); } return count; } else { int hashCode = fileStatus.getPath().toString().hashCode(); long modificationTime = fileStatus.getModificationTime(); long len = fileStatus.getLen(); BigInteger bi = BigInteger.valueOf(hashCode).add( BigInteger.valueOf(modificationTime).add(BigInteger.valueOf(len))); LOG.debug("File path hashcode [{0}], mod time [{1}], len [{2}] equals file code [{3}].", Integer.toString(hashCode), Long.toString(modificationTime), Long.toString(len), bi.toString(Character.MAX_RADIX)); return bi; } } protected void lookForCommandsToRegisterInClassPath() throws IOException { Enumeration<URL> systemResources = ClassLoader .getSystemResources(META_INF_SERVICES_ORG_APACHE_BLUR_COMMAND_COMMANDS); loadCommandClasses(systemResources, getClass().getClassLoader(), BigInteger.ZERO); } @SuppressWarnings("unchecked") protected void loadCommandClasses(Enumeration<URL> enumeration, ClassLoader loader, BigInteger version) throws IOException { Properties properties = new Properties(); while (enumeration.hasMoreElements()) { URL url = enumeration.nextElement(); InputStream inputStream = url.openStream(); properties.load(inputStream); inputStream.close(); } Set<Object> keySet = properties.keySet(); for (Object o : keySet) { String classNameToRegister = o.toString(); LOG.info("Loading class [{0}]", classNameToRegister); try { register((Class<? extends Command<?>>) loader.loadClass(classNameToRegister), version); } catch (ClassNotFoundException e) { throw new IOException(e); } } } @SuppressWarnings("unchecked") public Response reconnect(Long instanceExecutionId) throws IOException, TimeoutException { Future<Response> future = (Future<Response>) _driverRunningMap.get(instanceExecutionId); if (future == null) { throw new IOException("Execution instance id [" + instanceExecutionId + "] did not find any executing commands."); } try { Response response = future.get(_connectionTimeout, TimeUnit.MILLISECONDS); _driverRunningMap.remove(instanceExecutionId); return response; } catch (CancellationException e) { throw new IOException(e); } catch (InterruptedException e) { throw new IOException(e); } catch (ExecutionException e) { throw new IOException(e.getCause()); } catch (java.util.concurrent.TimeoutException e) { LOG.info("Timeout of command [{0}]", instanceExecutionId); throw new TimeoutException(instanceExecutionId); } } protected Response submitDriverCallable(Callable<Response> callable, Command<?> commandExecuting, CommandStatus originalCommandStatusObject, AtomicBoolean running) throws IOException, TimeoutException, ExceptionCollector { Future<Response> future = _executorServiceDriver.submit(callable); Long instanceExecutionId = getInstanceExecutionId(); _driverRunningMap.put(instanceExecutionId, new ResponseFuture<Response>(_runningCacheTombstoneTime, future, commandExecuting, originalCommandStatusObject, running)); try { Response response = future.get(_connectionTimeout, TimeUnit.MILLISECONDS); _driverRunningMap.remove(instanceExecutionId); return response; } catch (CancellationException e) { throw new IOException(e); } catch (InterruptedException e) { throw new IOException(e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof ExceptionCollector) { throw (ExceptionCollector) cause; } throw new IOException(cause); } catch (java.util.concurrent.TimeoutException e) { LOG.info("Timeout of command [{0}]", instanceExecutionId); throw new TimeoutException(instanceExecutionId); } } private Long getInstanceExecutionId() { synchronized (_random) { while (true) { Long id = _random.nextLong(); if (_driverRunningMap.containsKey(id)) { continue; } if (_workerRunningMap.containsKey(id)) { continue; } return id; } } } protected <T> Future<T> submitToExecutorService(Callable<T> callable, Command<?> commandExecuting, CommandStatus originalCommandStatusObject, AtomicBoolean running) { Future<T> future = _executorServiceWorker.submit(callable); Long instanceExecutionId = getInstanceExecutionId(); ResponseFuture<T> responseFuture = new ResponseFuture<T>(_runningCacheTombstoneTime, future, commandExecuting, originalCommandStatusObject, running); _workerRunningMap.put(instanceExecutionId, responseFuture); return responseFuture; } @Override public void close() throws IOException { _executorServiceWorker.shutdownNow(); _executorServiceDriver.shutdownNow(); if (_timer != null) { _timer.cancel(); _timer.purge(); } } public void register(Class<? extends Command<?>> commandClass, BigInteger version) throws IOException { try { Command<?> command = commandClass.newInstance(); _command.put(command.getName(), command); _commandLoadTime.put(command.getName(), version); _commandNameLookup.put(commandClass, command.getName()); LOG.info("Command [{0}] from class [{1}] registered.", command.getName(), commandClass.getName()); } catch (InstantiationException e) { throw new IOException(e); } catch (IllegalAccessException e) { throw new IOException(e); } } protected Command<?> getCommandObject(String commandName, ArgumentOverlay argumentOverlay) { Command<?> command = _command.get(commandName); if (command == null) { return null; } Command<?> clone = command.clone(); if (argumentOverlay == null) { return clone; } return argumentOverlay.setup(clone); } protected String getCommandName(Class<? extends Command<?>> clazz) { return _commandNameLookup.get(clazz); } // protected Map<String, Set<Shard>> getShards(TableContextFactory // tableContextFactory, Command<?> command, // final Args args, Set<String> tables) throws IOException { // Map<String, Set<Shard>> shardMap = new TreeMap<String, Set<Shard>>(); // if (command instanceof ShardRoute) { // ShardRoute shardRoute = (ShardRoute) command; // for (String table : tables) { // shardMap.put(table, // shardRoute.resolveShards(tableContextFactory.getTableContext(table), // args)); // } // } else { // if (tables.size() > 1) { // throw new IOException( // "Cannot route to single shard when multiple tables are specified. Implement ShardRoute on your command."); // } // String singleTable = tables.iterator().next(); // Set<Shard> shardSet = new TreeSet<Shard>(); // String shard = args.get("shard"); // if (shard == null) { // BlurArray shardArray = args.get("shards"); // if (shardArray != null) { // for (int i = 0; i < shardArray.length(); i++) { // shardSet.add(new Shard(singleTable, shardArray.getString(i))); // } // } // } else { // shardSet.add(new Shard(singleTable, shard)); // } // shardMap.put(singleTable, shardSet); // } // return shardMap; // } // protected Set<String> getTables(Command<?> command, final Args args) throws // IOException { // Set<String> tables = new TreeSet<String>(); // if (command instanceof TableRoute) { // TableRoute tableRoute = (TableRoute) command; // tables.addAll(tableRoute.resolveTables(args)); // } else { // if (args == null) { // return tables; // } // String table = args.get("table"); // if (table == null) { // BlurArray tableArray = args.get("tables"); // if (tableArray == null) { // return tables; // } // for (int i = 0; i < tableArray.length(); i++) { // tables.add(tableArray.getString(i)); // } // } else { // tables.add(table); // } // } // return tables; // } @SuppressWarnings("unchecked") public String getDescription(String commandName) { Command<?> command = getCommandObject(commandName, null); if (command == null) { return null; } Class<? extends Command<?>> clazz = (Class<? extends Command<?>>) command.getClass(); Description description = clazz.getAnnotation(Description.class); if (description == null) { return null; } return description.value(); } public String getReturnType(String commandName) { Command<?> command = getCommandObject(commandName, null); if (command == null) { return null; } return command.getReturnType(); } protected Arguments toArguments(Command<?> command) throws IOException { try { return CommandUtil.toArguments(command, _serDe); } catch (BlurException e) { throw new IOException(e); } } protected void validate(Command<?> command) throws IOException { String name = command.getName(); Command<?> cmd = getCommandObject(name, null); if (cmd == null) { throw new IOException("Command [" + name + "] not found."); } if (cmd.getClass().equals(command.getClass())) { return; } throw new IOException("Command [" + name + "] class does not match class registered."); } }