Package org.voltdb.processtools

Source Code of org.voltdb.processtools.ProcessSetManager$ShutdownThread

/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB L.L.C.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

package org.voltdb.processtools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.voltdb.types.TimestampType;
import org.voltdb.utils.Pair;

import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.utils.EventObservable;
import edu.brown.utils.EventObserver;
import edu.brown.utils.FileUtil;
import edu.brown.utils.StringUtil;
import edu.brown.utils.ThreadUtil;
import edu.brown.interfaces.Shutdownable;

public class ProcessSetManager implements Shutdownable {
    private static final Logger LOG = Logger.getLogger(ProcessSetManager.class);
    private static final LoggerBoolean debug = new LoggerBoolean();
    private static final LoggerBoolean trace = new LoggerBoolean();
    static {
        LoggerUtil.attachObserver(LOG, debug, trace);
    }
   
    private static final SimpleDateFormat BACKUP_FORMAT = new SimpleDateFormat("yyyy.MM.dd-HH:mm:ss");
   
    /**
     * How long to wait after a process starts before we will check whether
     * it's still alive.
     */
    private static final int POLLING_DELAY = 2000; // ms
   
    /**
     * Regular expressions of strings that we want to exclude from the remote
     * process' output. This is just to make the debug logs easier to read. 
     */
    public static final Pattern OUTPUT_CLEAN[] = {
        Pattern.compile("__(FILE|LINE)__[:]?"),
        Pattern.compile("[A-Z][\\w\\_]+\\.java:[\\d]+ "),
        Pattern.compile("^[\\s]+\\[java\\] ")
    };
   
    public enum StreamType { STDERR, STDOUT; }
   

    private static class ProcessData {
        final Process process;
        final OutputStreamWriter out;
        ProcessPoller poller;
        StreamWatcher stdout;
        StreamWatcher stderr;
       
        ProcessData(Process process) {
            this.process = process;
            this.out = new OutputStreamWriter(this.process.getOutputStream());
        }
    }

    /**
     *
     *
     */
    public final class OutputLine {
        OutputLine(String processName, StreamType stream, String value) {
            assert(value != null);
            this.processName = processName;
            this.stream = stream;
            this.value = value;
        }

        public final String processName;
        public final StreamType stream;
        public final String value;
       
        @Override
        public String toString() {
            return String.format("{%s, %s, \"%s\"}", processName, stream, value);
        }
    }

    class ProcessPoller extends Thread {
        final Process p;
        final String name;
        Boolean is_alive = null;
       
        public ProcessPoller(Process p, String name) {
            this.p = p;
            this.name = name;
            this.setDaemon(true);
            this.setPriority(MIN_PRIORITY);
        }
        @Override
        public void run() {
            try {
                this.is_alive = true;
                this.p.waitFor();
            } catch (InterruptedException ex) {
                // IGNORE
            } finally {
                synchronized (ProcessSetManager.this) {
                    if (debug.val && shutting_down == false) {
                        String msg = String.format("'%s' has stopped [wasAlive=%s]", this.name, this.is_alive);
                        LOG.warn(msg);
                    }
                } // SYNCH
                this.is_alive = false;
            }
        }
        public Boolean isProcessAlive() {
            return (this.is_alive);
        }
    } // END CLASS
   
    class ProcessSetPoller extends Thread {
        boolean reported_error = false;
        final Map<String, Long> delay = new HashMap<String, Long>();
       
        ProcessSetPoller() {
            this.setDaemon(true);
            this.setPriority(MIN_PRIORITY);
        }
        @Override
        public void run() {
            if (debug.val)
                LOG.debug("Starting ProcessSetPoller [initialDelay=" + initial_polling_delay + "]");
            final Set<String> toPoll = new HashSet<String>();
            while (true) {
                try {
                    Thread.sleep(2500);
                } catch (InterruptedException ex) {
                    if (shutting_down == false) ex.printStackTrace();
                    break;
                }
                // First figure out what processes that we want to poll
                // If we have a new entry, then we will want to wait a little bit to make
                // sure that it comes on-line
                toPoll.clear();
                long timestamp = System.currentTimeMillis();
                for (Entry<String, ProcessData> e : m_processes.entrySet()) {
                    // This is the first time that that we've seen it
                    if (this.delay.containsKey(e.getKey()) == false) {
                        this.delay.put(e.getKey(), timestamp + POLLING_DELAY);
                        if (debug.val) LOG.debug(String.format("Waiting %.1f seconds before polling '%s'",
                                                   POLLING_DELAY/1000d, e.getKey()));
                    }
                    // Otherwise, check whether the time has elapsed
                    else if (timestamp > this.delay.get(e.getKey())) {
                        toPoll.add(e.getKey());
                    }
                } // FOR
               
                for (String procName : toPoll) {
                    ProcessData pd = m_processes.get(procName);
                    if (pd.poller == null) continue;
                    Boolean isAlive = pd.poller.isProcessAlive();
                    if (isAlive == null) continue;
                    if (isAlive == false && reported_error == false && isShuttingDown() == false) {
                        String msg = String.format("Failed to poll '%s' [exitValue=%d]", procName, pd.process.exitValue());
                        LOG.error(msg);
                        msg = String.format("Process '%s' failed. Halting benchmark!", procName);
                        failure_observable.notifyObservers(msg);
                        reported_error = true;
                    }
                } // FOR
            } // WHILE
        }
    } // END CLASS
   
    /**
     * Thread that polls the given BufferedReader and parses its output.
     * The contents are written to the FileWriter and added to the output queue for
     * further processing
     */
    class StreamWatcher extends Thread {
        final BufferedReader reader;
        final String processName;
        final StreamType streamType;
        final AtomicBoolean expectDeath = new AtomicBoolean(false);
        final AtomicBoolean shutdownMsg = new AtomicBoolean(false);
        final FileWriter writer;

        StreamWatcher(BufferedReader reader, FileWriter writer, String processName, StreamType streamType) {
            assert(reader != null);
            this.setDaemon(true);
            this.reader = reader;
            this.writer = writer;
            this.processName = processName;
            this.streamType = streamType;
        }

        void setExpectDeath(boolean expectDeath) {
            this.expectDeath.set(expectDeath);
        }
       
        void shutdown(Throwable error) {
            if (this.expectDeath.get()) return;
           
            if (ProcessSetManager.this.shutting_down == false && this.shutdownMsg.compareAndSet(false, true)) {
                String msg = String.format("Stream monitoring thread for '%s' %s is exiting",
                                           this.processName, this.streamType);
                LOG.error(msg, (debug.val ? error : null));
                ProcessSetManager.this.failure_observable.notifyObservers(this.processName);
            }
        }

        @Override
        public void run() {
            try {
                while (true) {
                    String line = null;
                    try {
                        line = this.reader.readLine();
                    } catch (IOException e) {
                        this.shutdown(e);
                        return;
                    }
                   
                    // Skip empty input
                    if (line == null || line.isEmpty()) {
                        Thread.yield();
                        if (this.writer != null) this.writer.flush();
                        continue;
                    }
                       
                    // Remove stuff that we don't want printed
                    for (Pattern p : OUTPUT_CLEAN) {
                        Matcher m = p.matcher(line);
                        if (m != null) line = m.replaceAll("");
                    } // FOR

                    // Otherwise parse it so that somebody else can process it
                    OutputLine ol = new OutputLine(this.processName, this.streamType, line);
                    if (this.writer != null) {
                        synchronized (this.writer) {
                            this.writer.write(line + "\n");
                            this.writer.flush();
                        } // SYNCH
                    }
                    ProcessSetManager.this.m_output.add(ol);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
                System.exit(1);
            }
        }
    }
   
    // ============================================================================
    // INSTANCE MEMBERS
    // ============================================================================
   
    final int initial_polling_delay;
    final File output_directory;
    final EventObservable<String> failure_observable = new EventObservable<String>();
    final LinkedBlockingQueue<OutputLine> m_output = new LinkedBlockingQueue<OutputLine>();
    final Map<String, ProcessData> m_processes = new ConcurrentHashMap<String, ProcessData>();
    final ProcessSetPoller setPoller = new ProcessSetPoller();
    boolean shutting_down = false;
    boolean backup_logs = true;

    // ============================================================================
    // INITIALIZATION
    // ============================================================================

    /**
     * Constructor
     * @param log_dir
     * @param backup_logs
     * @param initial_polling_delay
     * @param failureObserver
     */
    public ProcessSetManager(String log_dir, boolean backup_logs, int initial_polling_delay, EventObserver<String> failureObserver) {
        this.output_directory = (log_dir != null && log_dir.isEmpty() == false ? new File(log_dir) : null);
        this.backup_logs = backup_logs;
        this.initial_polling_delay = initial_polling_delay;
        this.failure_observable.addObserver(failureObserver);
       
        if (this.output_directory != null)
            FileUtil.makeDirIfNotExists(this.output_directory.getAbsolutePath());
    }
   
    public ProcessSetManager() {
        this(null, false, 10000, null);
    }
   
    // ============================================================================
    // SHUTDOWN METHODS
    // ============================================================================

    /**
     * A list of all the processes that we have ever created
     * This is just to give us an extra chance to remove anything that we may have
     * left laying around if we fail to shutdown cleanly. 
     */
    private static Collection<Process> ALL_PROCESSES = new HashSet<Process>();
    private static class ShutdownThread extends Thread {
        @Override
        public void run() {
            synchronized(ProcessSetManager.class) {
                for (Process p : ALL_PROCESSES)
                    p.destroy();
            } // SYNCH
        }
    }
    static {
        Runtime.getRuntime().addShutdownHook(new ShutdownThread());
    }
   
   
    @Override
    public synchronized void prepareShutdown(boolean error) {
        this.shutting_down = true;
        for (String name : this.m_processes.keySet()) {
            ProcessData pd = this.m_processes.get(name);
            assert(pd!= null) : "Invalid process name '" + name + "'";
            pd.stdout.expectDeath.set(true);
            pd.stderr.expectDeath.set(true);
        } // FOR
    }
   
    @Override
    public synchronized void shutdown() {
        this.shutting_down = true;
        this.setPoller.interrupt();
        List<Runnable> runnables = new ArrayList<Runnable>();
        for (final String name : m_processes.keySet()) {
            runnables.add(new Runnable() {
                @Override
                public void run() {
                    killProcess(name);
                }
            });
        } // FOR
        if (runnables.isEmpty() == false) {
            if (debug.val)
                LOG.debug(String.format("Killing %d processes in parallel", runnables.size()));
            try {
                ThreadUtil.runNewPool(runnables);
            } catch (Throwable ex) {
                LOG.error("Unexpected error when shutting down processes", ex);
            }
            if (debug.val)
                LOG.debug("Finished shutting down");
        }
    }

    @Override
    public synchronized boolean isShuttingDown() {
        return (this.shutting_down);
    }

    // ============================================================================
    // UTILITY METHODS
    // ============================================================================
   
    public String[] getProcessNames() {
        String[] retval = new String[m_processes.size()];
        int i = 0;
        for (String clientName : m_processes.keySet())
            retval[i++] = clientName;
        return retval;
    }

    public int size() {
        return m_processes.size();
    }
   
    public OutputLine nextBlocking() {
        try {
            return m_output.take();
        } catch (InterruptedException e) {
            // if (this.shutting_down == false) e.printStackTrace();
        }
        return null;
    }

    public OutputLine nextNonBlocking() {
        return m_output.poll();
    }

    public void writeToAll(String cmd) {
        if (debug.val) LOG.debug(String.format("Sending %s to all processes", cmd));
        for (String processName : m_processes.keySet()) {
            this.writeToProcess(processName, cmd + "\n");
        }
    }
   
    public void writeToProcess(String processName, String data) {
        if (debug.val) LOG.debug(String.format("Writing '%s' to process %s", data.trim(), processName));
       
        // You always need a newline at the end of it to ensure that
        // it flushes properly
        if (data.endsWith("\n") == false) data += "\n";
       
        ProcessData pd = m_processes.get(processName);
        assert(pd != null) :
            "Missing ProcessData for '" + processName + "'";
        assert(pd.out != null) :
            "Missing OutputStreamWriter for '" + processName + "'";
        try {
            pd.out.write(data);
            pd.out.flush();
        } catch (IOException e) {
            if (processName.contains("client-")) return; // FIXME
            synchronized (this) {
                if (this.shutting_down == false) {
                    String msg = "";
                    if (data.trim().isEmpty()) {
                        msg = String.format("Failed to poll '%s'", processName);
                    } else {
                        msg = String.format("Failed to write '%s' command to '%s'", data.trim(), processName);
                    }
                    if (LOG.isDebugEnabled()) LOG.fatal(msg, e);
                    else LOG.fatal(msg, e);
                }
            } // SYNCH
            this.failure_observable.notifyObservers(processName);
        }
    }

    // ============================================================================
    // START PROCESS
    // ============================================================================

    /**
     * Main method for starting a new process under this manager
     * The given processName must be a unique handle that we will us to
     * identify this new process in the future. 
     * @param processName
     * @param cmd
     */
    public void startProcess(String processName, String[] cmd) {
        if (debug.val) LOG.debug("Starting Process: " + StringUtil.join(" ", cmd));
       
        ProcessBuilder pb = new ProcessBuilder(cmd);
        ProcessData pd = null;
        try {
            synchronized (ProcessSetManager.class) {
                if (m_processes.containsKey(processName)) {
                    throw new RuntimeException("Duplicate process name '" + processName + "'");
                }
                pd = new ProcessData(pb.start());
                m_processes.put(processName, pd);
                ALL_PROCESSES.add(pd.process);
            } // SYNCH
        } catch (IOException e) {
            throw new RuntimeException("Failed to start process '" + processName + "'", e);
        }
       
        BufferedReader out = new BufferedReader(new InputStreamReader(pd.process.getInputStream()));
        BufferedReader err = new BufferedReader(new InputStreamReader(pd.process.getErrorStream()));
       
        // Output File
        // We use a single output file for stdout and stderr
        FileWriter fw = null;
        if (this.output_directory != null) {
            String baseName = String.format("%s/%s.log", this.output_directory.getAbsolutePath(), processName);
            File path = new File(baseName);
           
            // 2012-01-24
            // If the file already exists, we'll move it out of the way automatically
            // if they want us to
            if (path.exists() && this.backup_logs) {
                Date log_date = new Date(path.lastModified());
                File backup_file = new File(baseName + "-" + BACKUP_FORMAT.format(log_date));
                path.renameTo(backup_file);
                if (debug.val)
                    LOG.debug(String.format("Moved log file '%s' to '%s'", path.getName(), backup_file.getName()));
            }
           
            try {
                fw = new FileWriter(path);
                fw.write("# " + new TimestampType().toString() + "\n");
            } catch (Exception ex) {
                throw new RuntimeException("Failed to create output writer for " + processName, ex);
            }
            if (debug.val)
                LOG.debug(String.format("Logging %s output to '%s'", processName, path));
        }
       
        pd.stdout = new StreamWatcher(out, fw, processName, StreamType.STDOUT);
        pd.stderr = new StreamWatcher(err, fw, processName, StreamType.STDERR);
       
        pd.stdout.start();
        pd.stderr.start();
       
        // Start the individual watching thread for this process
        pd.poller = new ProcessPoller(pd.process, processName);
        pd.poller.start();
        synchronized (this) {
            if (this.setPoller.isAlive() == false) this.setPoller.start();
        } // SYNCH
    }

    // ============================================================================
    // STOP PROCESSES
    // ============================================================================
   
    public synchronized Map<String, Integer> joinAll() {
        Map<String, Integer> retvals = new HashMap<String, Integer>();
        Long wait = 5000l;
        for (String processName : m_processes.keySet()) {
            Pair<Integer, Boolean> p = this.joinProcess(processName, wait);
            if (p.getSecond() && wait != null) {
                wait = (long)Math.ceil(wait.longValue() * 0.75);
                // if (wait < 1) wait = 1l;
            }
            retvals.put(processName, p.getFirst());
        } // FOR
        return (retvals);
    }
   
    public int joinProcess(String processName) {
        return this.joinProcess(processName, null).getFirst();
    }
   
    /**
     * Returns the status code + flag if we timed out
     * @param processName
     * @param millis
     * @return
     */
    public Pair<Integer, Boolean> joinProcess(String processName, Long millis) {
        final ProcessData pd = m_processes.get(processName);
        assert(pd != null);
        pd.stdout.expectDeath.set(true);
        pd.stderr.expectDeath.set(true);

        final CountDownLatch latch = new CountDownLatch(1);
        final String name = String.format("%s Killer [%s]", this.getClass().getSimpleName(), processName);
        Thread t = new Thread(name) {
            public void run() {
                try {
                    pd.process.waitFor();
                } catch (InterruptedException e) {
                    synchronized (this) {
                        if (shutting_down == false) e.printStackTrace();
                    } // SYNCH
                }
                latch.countDown();
            }
        };
        t.setDaemon(true);
        t.start();
       
        boolean timeout = false;
        try {
            if (millis != null) {
                timeout = (latch.await(millis, TimeUnit.MILLISECONDS) == false);
            } else {
                latch.await();
            }
        } catch (InterruptedException ex) {
            // Ignore...
        }
       
        int retval = this.killProcess(processName);
        return Pair.of(retval, timeout);
    }

    /**
     * Forcibly kill the process and return its exit code
     * @param processName
     * @return
     */
    public int killProcess(String processName) {
        ProcessData pd = m_processes.get(processName);
        if (pd != null) {
            if (pd.stdout != null) pd.stdout.expectDeath.set(true);
            if (pd.stderr != null) pd.stderr.expectDeath.set(true);
        }
        int retval = -255;

        if (debug.val) LOG.debug("Killing '" + processName + "'");
        pd.process.destroy();
        try {
            pd.process.waitFor();
            retval = pd.process.exitValue();
        } catch (InterruptedException e) {
            synchronized (this) {
                if (this.shutting_down == false) e.printStackTrace();
            } // SYNCH
        }

        synchronized(ProcessSetManager.class) {
            ALL_PROCESSES.remove(pd.process);
            pd.poller.interrupt();
        } // SYNCH

        return retval;
    }
   
    public static void main(String[] args) {
        ProcessSetManager psm = new ProcessSetManager();
        psm.startProcess("ping4c", new String[] { "ping", "volt4c" });
        psm.startProcess("ping3c", new String[] { "ping", "volt3c" });
        while(true) {
            OutputLine line = psm.nextBlocking();
            System.out.printf("(%s:%s): %s\n", line.processName, line.stream.name(), line.value);
        }
    }

}
TOP

Related Classes of org.voltdb.processtools.ProcessSetManager$ShutdownThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.