org.taverna.server.master.localworker.ForkRunFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.master.localworker.ForkRunFactory.java

Source

/*
 * Copyright (C) 2010-2011 The University of Manchester
 * 
 * See the file "LICENSE.txt" for license terms.
 */
package org.taverna.server.master.localworker;

import static java.lang.System.getProperty;
import static java.lang.Thread.sleep;
import static java.util.Arrays.asList;
import static java.util.Calendar.SECOND;
import static java.util.UUID.randomUUID;
import static org.apache.commons.logging.LogFactory.getLog;
import static org.springframework.jmx.support.MetricType.COUNTER;
import static org.taverna.server.master.TavernaServerImpl.JMX_ROOT;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.rmi.ConnectException;
import java.rmi.ConnectIOException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.Calendar;
import java.util.UUID;

import javax.xml.bind.JAXBException;

import org.apache.commons.logging.Log;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.taverna.server.localworker.remote.RemoteRunFactory;
import org.taverna.server.localworker.remote.RemoteSingleRun;
import org.taverna.server.master.common.Workflow;
import org.taverna.server.master.exceptions.NoCreateException;
import org.taverna.server.master.factories.ConfigurableRunFactory;
import org.taverna.server.master.utils.UsernamePrincipal;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * A simple factory for workflow runs that forks runs from a subprocess.
 * 
 * @author Donal Fellows
 */
@ManagedResource(objectName = JMX_ROOT + "RunFactory", description = "The factory for simple singleton forked run.")
public class ForkRunFactory extends AbstractRemoteRunFactory implements ConfigurableRunFactory {
    private int lastStartupCheckCount;
    private Integer lastExitCode;
    private int totalRuns;
    private RemoteRunFactory factory;
    private Process factoryProcess;
    private String factoryProcessName;

    /**
     * Create a factory for remote runs that works by forking off a subprocess.
     * 
     * @throws JAXBException
     *             Shouldn't happen.
     */
    public ForkRunFactory() throws JAXBException {
    }

    private void reinitFactory() {
        boolean makeFactory = factory != null;
        killFactory();
        try {
            if (makeFactory)
                initFactory();
        } catch (Exception e) {
            log.fatal("failed to make connection to remote run factory", e);
        }
    }

    /** @return Which java executable to run. */
    @ManagedAttribute(description = "Which java executable to run.", currencyTimeLimit = 300)
    @Override
    public String getJavaBinary() {
        return state.getJavaBinary();
    }

    /**
     * @param javaBinary
     *            Which java executable to run.
     */
    @ManagedAttribute(description = "Which java executable to run.", currencyTimeLimit = 300)
    @Override
    public void setJavaBinary(String javaBinary) {
        state.setJavaBinary(javaBinary);
        reinitFactory();
    }

    /** @return The list of additional arguments used to make a worker process. */
    @ManagedAttribute(description = "The list of additional arguments used to make a worker process.", currencyTimeLimit = 300)
    @Override
    public String[] getExtraArguments() {
        return state.getExtraArgs();
    }

    /**
     * @param firstArguments
     *            The list of additional arguments used to make a worker
     *            process.
     */
    @ManagedAttribute(description = "The list of additional arguments used to make a worker process.", currencyTimeLimit = 300)
    @Override
    public void setExtraArguments(String[] firstArguments) {
        state.setExtraArgs(firstArguments);
        reinitFactory();
    }

    /** @return The location of the JAR implementing the server worker process. */
    @ManagedAttribute(description = "The location of the JAR implementing the server worker process.")
    @Override
    public String getServerWorkerJar() {
        return state.getServerWorkerJar();
    }

    /**
     * @param serverWorkerJar
     *            The location of the JAR implementing the server worker
     *            process.
     */
    @ManagedAttribute(description = "The location of the JAR implementing the server worker process.")
    @Override
    public void setServerWorkerJar(String serverWorkerJar) {
        state.setServerWorkerJar(serverWorkerJar);
        reinitFactory();
    }

    /** @return The script to run to start running a workflow. */
    @ManagedAttribute(description = "The script to run to start running a workflow.", currencyTimeLimit = 300)
    @Override
    public String getExecuteWorkflowScript() {
        return state.getExecuteWorkflowScript();
    }

    /**
     * @param executeWorkflowScript
     *            The script to run to start running a workflow.
     */
    @ManagedAttribute(description = "The script to run to start running a workflow.", currencyTimeLimit = 300)
    @Override
    public void setExecuteWorkflowScript(String executeWorkflowScript) {
        state.setExecuteWorkflowScript(executeWorkflowScript);
        reinitFactory();
    }

    /** @return How many seconds to wait for a worker process to register itself. */
    @ManagedAttribute(description = "How many seconds to wait for a worker process to register itself.", currencyTimeLimit = 300)
    @Override
    public int getWaitSeconds() {
        return state.getWaitSeconds();
    }

    /**
     * @param seconds
     *            How many seconds to wait for a worker process to register
     *            itself.
     */
    @ManagedAttribute(description = "How many seconds to wait for a worker process to register itself.", currencyTimeLimit = 300)
    @Override
    public void setWaitSeconds(int seconds) {
        state.setWaitSeconds(seconds);
    }

    /**
     * @return How many milliseconds to wait between checks to see if a worker
     *         process has registered.
     */
    @ManagedAttribute(description = "How many milliseconds to wait between checks to see if a worker process has registered.", currencyTimeLimit = 300)
    @Override
    public int getSleepTime() {
        return state.getSleepMS();
    }

    /**
     * @param sleepTime
     *            How many milliseconds to wait between checks to see if a
     *            worker process has registered.
     */
    @ManagedAttribute(description = "How many milliseconds to wait between checks to see if a worker process has registered.", currencyTimeLimit = 300)
    @Override
    public void setSleepTime(int sleepTime) {
        state.setSleepMS(sleepTime);
    }

    /**
     * @return How many checks were done for the worker process the last time a
     *         spawn was tried.
     */
    @ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
    @Override
    public int getLastStartupCheckCount() {
        return lastStartupCheckCount;
    }

    /** @return How many times has a workflow run been spawned by this engine. */
    @ManagedMetric(description = "How many times has a workflow run been spawned by this engine.", currencyTimeLimit = 10, metricType = COUNTER, category = "throughput")
    @Override
    public int getTotalRuns() {
        return totalRuns;
    }

    /**
     * @return What was the exit code from the last time the factory subprocess
     *         was killed?
     */
    @ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
    @Override
    public Integer getLastExitCode() {
        return lastExitCode;
    }

    /**
     * @return What the factory subprocess's main RMI interface is registered
     *         as.
     */
    @ManagedAttribute(description = "What the factory subprocess's main RMI interface is registered as.", currencyTimeLimit = 60)
    @Override
    public String getFactoryProcessName() {
        return factoryProcessName;
    }

    /**
     * Makes the subprocess that manufactures runs.
     * 
     * @throws Exception
     *             If anything goes wrong.
     */
    public void initFactory() throws Exception {
        if (factory != null)
            return;
        // Generate the arguments to use when spawning the subprocess
        factoryProcessName = state.getFactoryProcessNamePrefix() + randomUUID();
        ProcessBuilder p = new ProcessBuilder(getJavaBinary());
        p.command().addAll(asList(getExtraArguments()));
        p.command().add("-jar");
        p.command().add(getServerWorkerJar());
        p.command().add(getExecuteWorkflowScript());
        p.command().add(factoryProcessName);
        p.redirectErrorStream(true);
        p.directory(new File(getProperty("javax.servlet.context.tempdir", getProperty("java.io.tmpdir"))));

        // Spawn the subprocess
        log.info("about to create subprocess: " + p.command());
        factoryProcess = p.start();
        Thread logger = new Thread(new OutputLogger(factoryProcessName, factoryProcess),
                factoryProcessName + ".Logger");
        logger.setDaemon(true);
        logger.start();

        // Wait for the subprocess to register itself in the RMI registry
        Calendar deadline = Calendar.getInstance();
        deadline.add(SECOND, state.getWaitSeconds());
        Exception lastException = null;
        lastStartupCheckCount = 0;
        while (deadline.after(Calendar.getInstance())) {
            try {
                sleep(state.getSleepMS());
                lastStartupCheckCount++;
                log.info("about to look up resource called " + factoryProcessName);
                try {
                    // Validate registry connection first
                    getTheRegistry().list();
                } catch (ConnectException ce) {
                    log.warn("connection problems with registry", ce);
                } catch (ConnectIOException e) {
                    log.warn("connection problems with registry", e);
                }
                factory = (RemoteRunFactory) getTheRegistry().lookup(factoryProcessName);
                log.info("successfully connected to factory subprocess " + factoryProcessName);
                if (interhost != null)
                    factory.setInteractionServiceDetails(interhost, interport, interwebdav, interfeed);
                return;
            } catch (InterruptedException ie) {
                continue;
            } catch (NotBoundException nbe) {
                lastException = nbe;
                log.info("resource \"" + factoryProcessName + "\" not yet registered...");
                continue;
            } catch (RemoteException re) {
                // Unpack a remote exception if we can
                lastException = re;
                try {
                    if (re.getCause() != null)
                        lastException = (Exception) re.getCause();
                } catch (Throwable t) {
                    // Ignore!
                }
            } catch (Exception e) {
                lastException = e;
            }
        }
        throw lastException;
    }

    private static class OutputLogger implements Runnable {
        private final Log log;

        OutputLogger(String name, Process process) {
            log = getLog("Taverna.Server.LocalWorker." + name);
            this.uniqueName = name;
            this.br = new BufferedReader(new InputStreamReader(process.getInputStream()));
        }

        private String uniqueName;
        private BufferedReader br;

        @Override
        public void run() {
            try {
                String line;
                while (true) {
                    line = br.readLine();
                    if (line == null)
                        break;
                    log.info(uniqueName + " subprocess output: " + line);
                }
            } catch (IOException e) {
                // Do nothing...
            } catch (Exception e) {
                log.warn("failure in reading from " + uniqueName, e);
            } finally {
                try {
                    br.close();
                } catch (Throwable e) {
                }
            }
        }
    }

    /**
     * Destroys the subprocess that manufactures runs.
     */
    public void killFactory() {
        if (factory != null) {
            log.info("requesting shutdown of " + factoryProcessName);
            try {
                factory.shutdown();
                sleep(700);
            } catch (RemoteException e) {
                log.warn(factoryProcessName + " failed to shut down nicely", e);
            } catch (InterruptedException e) {
                log.debug("interrupted during wait after asking " + factoryProcessName + " to shut down", e);
            } finally {
                factory = null;
            }
        }

        if (factoryProcess != null) {
            int code = -1;
            try {
                lastExitCode = code = factoryProcess.exitValue();
                log.info(factoryProcessName + " already dead?");
            } catch (RuntimeException e) {
                log.info("trying to force death of " + factoryProcessName);
                try {
                    factoryProcess.destroy();
                    sleep(350); // takes a little time, even normally
                    lastExitCode = code = factoryProcess.exitValue();
                } catch (Exception e2) {
                    code = -1;
                }
            } finally {
                factoryProcess = null;
            }
            if (code > 128) {
                log.info(factoryProcessName + " died with signal=" + (code - 128));
            } else if (code >= 0) {
                log.info(factoryProcessName + " process killed: code=" + code);
            } else {
                log.warn(factoryProcessName + " not yet dead");
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        killFactory();
        super.finalize();
    }

    /**
     * The real core of the run builder, factored out from its reliability
     * support.
     * 
     * @param creator
     *            Who created this workflow?
     * @param wf
     *            The serialized workflow.
     * @return The remote handle of the workflow run.
     * @throws RemoteException
     *             If anything fails (communications error, etc.)
     */
    private RemoteSingleRun getRealRun(@NonNull UsernamePrincipal creator, @NonNull String wf, UUID id)
            throws RemoteException {
        String globaluser = "Unknown Person";
        if (creator != null)
            globaluser = creator.getName();
        RemoteSingleRun rsr = factory.make(wf, globaluser, makeURReciver(creator), id);
        totalRuns++;
        return rsr;
    }

    @Override
    protected RemoteSingleRun getRealRun(UsernamePrincipal creator, Workflow workflow, UUID id) throws Exception {
        String wf = serializeWorkflow(workflow);
        for (int i = 0; i < 3; i++) {
            if (factory == null)
                initFactory();
            try {
                return getRealRun(creator, wf, id);
            } catch (ConnectException e) {
                // factory was lost; try to recreate
            } catch (ConnectIOException e) {
                // factory was lost; try to recreate
            }
            killFactory();
        }
        throw new NoCreateException(
                "total failure to connect to factory " + factoryProcessName + "despite attempting restart");
    }

    @Override
    public String getPasswordFile() {
        return "";
    }

    @Override
    public String getServerForkerJar() {
        return "<NOT-SUPPORTED>";
    }

    @Override
    public void setPasswordFile(String newValue) {
        // Do nothing
    }

    @Override
    public void setServerForkerJar(String newValue) {
        // Do nothing
    }

    @Override
    public String[] getFactoryProcessMapping() {
        return new String[0];
    }
}