package com.netflix.priam.agent.process; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.netflix.priam.agent.AgentConfiguration; import com.netflix.priam.agent.NodeStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.concurrent.GuardedBy; import javax.inject.Provider; import java.io.Closeable; import java.util.Deque; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingDeque; /** * Manages running processes in the agent */ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") public class AgentProcessManager implements Closeable { private static final Logger logger = LoggerFactory.getLogger(AgentProcessManager.class); private final AgentProcessMap processMap; private final AgentConfiguration configuration; private final Provider<NodeStatus> nodeToolProvider; private final ConcurrentMap<String, ProcessRecord> activeProcesses = Maps.newConcurrentMap(); private final ExecutorService executorService; @GuardedBy("synchronized") private final Deque<ProcessRecord> completedProcesses = new LinkedBlockingDeque<ProcessRecord>(); /** * @param processMap map from process name to process provider * @param configuration config * @param nodeToolProvider provider for the Node Tool operations */ public AgentProcessManager(AgentProcessMap processMap, AgentConfiguration configuration, Provider<NodeStatus> nodeToolProvider) { this.processMap = processMap; this.configuration = configuration; this.nodeToolProvider = nodeToolProvider; executorService = Executors.newFixedThreadPool(configuration.getMaxProcessThreads(), new ThreadFactoryBuilder().setDaemon(true).setNameFormat("AgentProcessManager-%d").build()); } /** * List of currently executing processes * * @return processes */ public List<ProcessRecord> getActiveProcesses() { return ImmutableList.copyOf(activeProcesses.values()); } /** * List of completed processes * * @return processes */ public List<ProcessRecord> getCompletedProcesses() { synchronized(completedProcesses) { return ImmutableList.copyOf(completedProcesses); } } /** * Start a process * * @param name name of the process to start (must exist in the process map) * @param id ID of the process. IDs must be unique. If there is already a process running with this ID this method will not start a new process. * @param arguments arguments for the processes * @return true if a new process was started * @throws Exception errors */ public boolean startProcess(String name, String id, String[] arguments) throws Exception { ProcessRecord newProcessRecord = new ProcessRecord(name, id, arguments); ProcessRecord oldProcessRecord = activeProcesses.putIfAbsent(id, newProcessRecord); final ProcessRecord useProcessRecord = (oldProcessRecord != null) ? oldProcessRecord : newProcessRecord; synchronized(useProcessRecord) { if ( useProcessRecord.getExecutor() == null ) { AgentProcess process = processMap.newProcess(name); validateArguments(process, arguments); Future<Void> future = executorService.submit(new Executor(this, id, process, nodeToolProvider.get(), arguments)); useProcessRecord.setExecutor(future); return true; } return false; } } /** * Attempt to stop the process with the given ID * * @param id ID of the process to stop * @return true if the process was found */ public boolean stopProcess(String id) { final ProcessRecord processRecord = activeProcesses.get(id); if ( processRecord == null ) { return false; } synchronized(processRecord) { processRecord.noteStopAttempt(); Future<Void> executor = processRecord.getExecutor(); if ( executor != null ) { executor.cancel(true); } } return true; } /** * Stop all process and block until they complete or until time runs out * * @param timeout max time to wait for process completion * @param unit time unit * @return true if all processes terminated * @throws InterruptedException if interrupted */ public boolean closeAndWaitForCompletion(long timeout, TimeUnit unit) throws InterruptedException { close(); return executorService.awaitTermination(timeout, unit); } @Override public void close() { executorService.shutdownNow(); } void removeProcess(String id) { final ProcessRecord processRecord = activeProcesses.remove(id); if ( processRecord == null ) { return; } synchronized(processRecord) { processRecord.setEnd(); } synchronized(completedProcesses) { try { while ( completedProcesses.size() >= configuration.getMaxCompletedProcesses() ) { completedProcesses.removeLast(); } } catch ( NoSuchElementException ignore ) { if(logger.isTraceEnabled()) logger.trace("nothing left in list"); } completedProcesses.addFirst(processRecord); } } private void validateArguments(AgentProcess process, String[] arguments) throws IncorrectArgumentsException { ProcessMetaData metaData = process.getMetaData(); if ( arguments.length < metaData.getMinArguments() ) { throw new IncorrectArgumentsException("Expected at least " + metaData.getMinArguments() + " arguments but was only provided " + arguments.length); } } }