package org.oddjob; import org.apache.log4j.Logger; import org.oddjob.framework.StopWait; import org.oddjob.state.StateEvent; /** * A Wrapper for running Oddjob that ensures a smooth shutdown. * <p> * If Oddjob doesn't terminate then a timeout will just kill the JVM. The * timeout is configurable but defaults to 15 seconds. * * @author rob * */ public class OddjobRunner implements Runnable { private static final Logger logger = Logger.getLogger(OddjobRunner.class); public static final String KILLER_TIMEOUT_PROPERTY = "oddjob.shutdown.killer.timeout"; public static final long DEFAULT_KILLER_TIMEOUT = 15000L; /** The Oddjob we're running. */ private final Oddjob oddjob; /** Flag if Oddjob is being destroyed from the Shutdown Hook. */ private volatile boolean destroying = false; /** The killer thread time out. */ private final long killerTimeout; private final ExitHandler exitHandler; public interface ExitHandler { public void exit(int exitStatus); } public OddjobRunner(Oddjob oddjob) { this(oddjob, new ExitHandler() { @Override public void exit(int exitStatus) { // uses halt, not exit as we don't want to invoke the // shutdown hook Runtime.getRuntime().halt(exitStatus); } }); } /** * Constructor. * * @param oddjob The Oddjob to run. */ public OddjobRunner(Oddjob oddjob, ExitHandler exitHandler) { if (oddjob == null) { throw new NullPointerException("No Oddjob"); } if (exitHandler == null) { throw new NullPointerException("No Exit Handler"); } this.oddjob = oddjob; this.exitHandler = exitHandler; String timeoutProperty = System.getProperty(KILLER_TIMEOUT_PROPERTY); if (timeoutProperty == null) { killerTimeout = DEFAULT_KILLER_TIMEOUT; } else { killerTimeout = Long.parseLong(timeoutProperty); } } public Oddjob getOddjob() { return oddjob; } /** * Initialise a shutdown hook. A separate method so that run can * be tested without a shutdown hook. */ public void initShutdownHook() { Runtime.getRuntime().addShutdownHook(new ShutdownHook()); } public void run() { logger.info("Starting Oddjob version " + oddjob.getVersion()); try { oddjob.run(); // This needs to be thought out a bit more. // The logic goes along the lines of if the main // thread get's here we need to wait until the Oddjob // has completed and then we can destroy it. // We do this by stopping the executors, and leave the destroying // of Oddjob to the Shutdown hook. if (destroying) { logger.debug("Oddjob execution thread complete via destroy from the shutdown hook."); } else { logger.debug("Oddjob execution thread completed." + " May wait for Oddjob, it's state is " + oddjob.lastStateEvent().getState() + "."); // Possibly wait for Oddjob to be in a stopped state. new StopWait(oddjob, Long.MAX_VALUE).run(); logger.debug("Oddjob is finished, state [" + oddjob.lastStateEvent().getState() + "]"); // Stopping executors should allow JVM to exit. The shutdown // thread will take care of destroying Oddjob. oddjob.stopExecutors(); } } catch (Throwable t) { logger.fatal("Exception running Oddjob.", t); exitHandler.exit(1); } } class Killer implements Runnable { @Override public void run() { logger.debug("Killer thread started. Oddjob has " + killerTimeout + "ms to stop niceley."); try { Thread.sleep(killerTimeout); } catch (InterruptedException e) { logger.debug("Killer thread interrupted and terminating."); return; } logger.error("Failed to stop Oddjob nicely, using halt(-1)"); exitHandler.exit(-1); } } /** * Oddjob's shutdown hook. * <p> * This Class has evolved quite a lot though trial and error due to * a lack of understanding of JVM shutdown. Should this thread be a * daemon? Current thinking is no because you don't want other daemon * threads to terminate until Oddjob has been shutdown properly. * */ class ShutdownHook extends OddjobShutdownThread { /** Killer thread will forcibly halt Oddjob if it hasn't terminated * cleanly. */ private Thread killer; /* * (non-Javadoc) * @see java.lang.Thread#run() */ public void run() { logger.info("Shutdown Hook Executing."); // killer will just kill process if we can't stop in 15 sec killer = new Thread(new Killer(), "Killer-Thread"); // start the killer. Not sure it really need to be daemon but // it does no harm. logger.debug("Starting killer thread."); killer.setDaemon(true); killer.start(); StateEvent lastStateEvent = oddjob.lastStateEvent(); logger.debug("Destroying Oddjob."); destroying = true; oddjob.destroy(); // Nothing's hanging so we don't need our killer. killer.interrupt(); // determine exit status. org.oddjob.state.State state = lastStateEvent.getState(); if (state.isException()) { logger.error("Oddjob terminating JVM with status -1. Oddjob state [" + state + "].", lastStateEvent.getException()); // really really bad but how else to get the state out to the OS. exitHandler.exit(-1); } else if (state.isIncomplete()) { logger.info("Oddjob terminating JVM with status 1. Oddjob state [" + state + "]."); exitHandler.exit(1); } else { logger.info("Oddjob complete. Oddjob state [" + state + "]."); } } } }