/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2002 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package alma.acs.concurrent; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; /** * This class allows running some task repeatedly with a given delay. * It is similar to the loop in C++ class ACS::Thread. * <p> * The code of the task to be executed gets passed to the constructor as a {@link Runnable} object. * The time between executions is given in the constructor and can later be changed through * {@link #setDelayTime(long, TimeUnit)}. * <p> * If the task takes some time to execute, you should consider implementing a soft cancel option. To do this, * extend {@link #CancelableRunnable} instead of simply implementing <code>Runnable</code>. * When the action should be canceled (e.g. due to {@link #shutdown(long, TimeUnit)}), the <code>shouldTerminate</code> * flag will be set. Your code should check this flag at convenient times, and should return if the flag is set. * Alternatively you can override the cancel method and terminate the task thread in different ways. * <p> * Method {@link #runLoop()} starts the loop. * Optionally the loop can be stopped with {@link #suspendLoop()} or {@link #suspendLoopAndWait(long, TimeUnit)}, * and can also be restarted with another call to {@link #runLoop()}. * To stop the loop for good, call {@link #shutdown(long, TimeUnit)}. * <p> * While the underlying classes from the JDK <code>concurrent</code> package could also be used directly, * this class allows for shorter code that is also more similar to the style used in C++. * Especially it imposes the limit of running one task repeatedly, which gives an easier API, * at the expense of creating a separate instance of ThreadLoopRunner for some other repeated task. * (The older JDK class {@link java.util.Timer} has problems recovering from errors and should not be used.) * * @author hsommer */ public class ThreadLoopRunner { private final Logger logger; /** * The single-threaded executor that backs our ThreadLoopRunner. */ private final ScheduledExecutorService runner; /** * Wraps the user-supplied task, to enable this class to synchronize * with the task execution that otherwise is delegated to standard JDK {@link #runner}. */ private final TaskWrapper taskWrapper; public static enum ScheduleDelayMode {FIXED_RATE, FIXED_DELAY} private volatile ScheduleDelayMode delayMode; /** * Delay time in ns. */ private volatile long delayTimeNanos; /** * The task loop. Must be protected by {@link #loopLock}. */ private volatile ScheduledFuture<?> loop; /** * A flag set by {@link #shutdown(long, TimeUnit)} and used by other methods * to throw <code>IllegalStateException</code> during or after shutdown. * It can be queried by {@link #isDisabled()}. */ private final AtomicBoolean isDefunct; /** * A lock to synchronize on changes of the task loop ({@link #loop}). */ private final ReentrantLock loopLock = new ReentrantLock(); /** * Used to make logs and thread names more readable. May be null. */ private final String loopName; /** * Creates a <code>ThreadLoopRunner</code> that can repeatedly execute <code>task</code>. * The mode defaults to {@link ScheduleDelayMode#FIXED_RATE} unless being changed * via {@link #setDelayMode(ScheduleDelayMode)}. * * @param task user-supplied {@link Runnable}, or better subtype {@link ThreadLoopRunner.CancelableRunnable}. * @param delayTime * @param unit * @param tf ThreadFactory from which the loop thread will be created. * @param logger Logger used by this class. * @param name Facilitates debugging, by using a meaningful name for logs and threads. */ public ThreadLoopRunner(Runnable task, long delayTime, TimeUnit unit, final ThreadFactory tf, Logger logger, String name) { this.logger = logger; this.loopName = ( (name != null && !name.trim().isEmpty()) ? name.trim() : null ); this.runner = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory(tf, loopName)); this.taskWrapper = new TaskWrapper(task, loopLock, logger); this.delayMode = ScheduleDelayMode.FIXED_RATE; isDefunct = new AtomicBoolean(false); setDelayTime(delayTime, unit); } /** * @return The delay time in ms, as set in the constructor or changed afterwards * in {@link #setDelayTime(long, TimeUnit)}. */ public long getDelayTimeMillis() { return TimeUnit.MILLISECONDS.convert(delayTimeNanos, TimeUnit.NANOSECONDS); } /** * Sets the time between calls to the loop action object, * where the time for the task itself is included or not, * depending on the chosen {@link ScheduleDelayMode}. * <p> * If this method is called while the thread loop is already running, * the task will be run again right after the currently running task * has finished; only after that run we'll get into the proper delay timing. * <p> * Implementation note: If this method is called while the thread loop is already running, * it will stop the loop, apply the new value, and then re-start the loop. * It is a limitation in the underlying {@link ScheduledThreadPoolExecutor} * that the delay time cannot be changed without stopping and restarting the loop (= repetitive task). * If this becomes a problem, we could use the concurrent lib classes in a more customized way. * * @param delayTime new delay time * @param unit * @see #setDelayMode(ScheduleDelayMode) */ public void setDelayTime(final long delayTime, final TimeUnit unit) { if (isDefunct.get()) { throw new IllegalStateException("["+ loopName + "] already disabled"); } loopLock.lock(); try { // store the value delayTimeNanos = TimeUnit.NANOSECONDS.convert(delayTime, unit); if (isLoopRunning()) { // stop the loop suspendLoop(); // restart the loop if (isTaskRunning()) { // schedule restarting the loop after the currently running task has finished. // The same loopLock is used so that setting up this task now guarantees getting it run after the currently executing task. taskWrapper.restartLoopAfterCurrentTaskFinished(new Runnable() { @Override public void run() { logger.finer("["+ loopName + "] will restart the loop now, which was stopped to change the delay time."); // @TODO: here we could sleep a bit to keep the intended delay rate/length, if needed. runLoop(); } }); } else { // The task was not running after we suspended the loop. We thus assume that no task has been started in the meantime // and simply restart the loop directly. runLoop(); } } } finally { loopLock.unlock(); } } /** * @return The delay mode, either in use if the loop is already running, * or to be used when calling {@link #runLoop()}. */ public ScheduleDelayMode getDelayMode() { return delayMode; } /** * Sets the delay mode to be used for the next {@link #runLoop()}. * <p> * This method must not be called when the loop is already running (see {@link #isLoopRunning()}), * in which case it throws an IllegalStateException. * The reason for this is that we see no need to change this mode on the fly, * and rather avoid the overhead of automatically stopping and restarting the loop * with the possible complications if the run() method does not terminate. * Also we don't want {@link #getDelayMode()} to give results that are not correct for the currently running loop. * Note that the same issue is handled differently in {@link #setDelayTime(long, TimeUnit)} * where it seems desirable to change the delay time while the loop is running. * * @param delayMode * FIXED_RATE or FIXED_DELAY, * see {@link ScheduledExecutorService#scheduleAtFixedRate(Runnable, long, long, TimeUnit)} and * {@link ScheduledExecutorService#scheduleWithFixedDelay(Runnable, long, long, TimeUnit)}. * Note that the C++ implementation uses equivalent of FIXED_RATE. * @throws IllegalStateException if called when the loop is running, or after shutdown. */ public void setDelayMode(ScheduleDelayMode delayMode) { if (isDefunct.get()) { throw new IllegalStateException("["+ loopName + "] already disabled"); } if (delayMode == null) { throw new IllegalArgumentException("delayMode must not be null"); } if (isLoopRunning()) { throw new IllegalStateException("Cannot set delay mode while the loop is running"); } this.delayMode = delayMode; } /** * Runs the loop, either for the first time, or after a call to {@link #suspendLoop()}. * @throws IllegalStateException * if the loop is already running, * or if the <code>run()</code> method of a previous loop is still executing, * or after shutdown * @see #isLoopRunning() */ public void runLoop() { if (isDefunct.get()) { throw new IllegalStateException("["+ loopName + "] already disabled"); } loopLock.lock(); try { if (isLoopRunning()) { throw new IllegalStateException("Loop is already running"); } if (isTaskRunning()) { throw new IllegalStateException("The task's run method is still being executed"); } if (delayMode == ScheduleDelayMode.FIXED_RATE) { loop = runner.scheduleAtFixedRate(taskWrapper, 0, delayTimeNanos, TimeUnit.NANOSECONDS); logger.finer("["+ loopName + "] started task loop with FIXED_RATE=" + TimeUnit.MILLISECONDS.convert(delayTimeNanos, TimeUnit.NANOSECONDS) + " ms."); } else { loop = runner.scheduleWithFixedDelay(taskWrapper, 0, delayTimeNanos, TimeUnit.NANOSECONDS); logger.finer("["+ loopName + "] started task loop with FIXED_DELAY=" + TimeUnit.MILLISECONDS.convert(delayTimeNanos, TimeUnit.NANOSECONDS) + " ms."); } } finally { loopLock.unlock(); } } /** * @return <code>true</code> if the loop is running, regardless of whether the task is currently being executed. */ public boolean isLoopRunning() { loopLock.lock(); try { return (loop != null); } finally { loopLock.unlock(); } } /** * @return <code>true</code> if the task is running, regardless of whether the loop is still running or has been stopped already. * @see #suspendLoopAndWait(long, TimeUnit) */ public boolean isTaskRunning() { return taskWrapper.isRunning(); } /** * Returns <code>true</code> after {@link #shutdown(long, TimeUnit)} was called. * Then invoking any control method of this class will throw an IllegalStateException. */ public boolean isDisabled() { return isDefunct.get(); } /** * Stops the loop, without attempting to cancel the possibly running action even if it was provided * as a {@link CancelableRunnable}. * Note also that this call returns quickly, without waiting for a possibly running action to finish. * <p> * The loop can be started again later via {@link #runLoop()}, once the running task has finished. * Suspending and restarting the loop does not lead to the creation of a new Thread. * * @throws IllegalStateException if called after shutdown. * @see #suspendLoopAndWait(long, TimeUnit) */ public void suspendLoop() { if (isDefunct.get()) { throw new IllegalStateException("["+ loopName + "] already disabled"); } loopLock.lock(); try { if (isLoopRunning()) { if (loop.cancel(false)) { logger.finer("["+ loopName + "] suspended the task loop."); } else { logger.fine("["+ loopName + "] failed to suspend the loop (without having attempted to cancel a running task, if any)."); } loop = null; // also remove special post-run task, if any this.taskWrapper.restartLoopAfterCurrentTaskFinished(null); } else { logger.fine("["+ loopName + "] loop was not running, nothing to suspend."); } } finally { loopLock.unlock(); } } /** * Like {@link #suspendLoop()}, but additionally waits for the currently running task (if any) * to finish, with the given timeout applied. * <p> * If there is a task running and it fails to terminate, * a subsequent call to {@link #runLoop()} will fail with an IllegalStateException. * * @param timeout * @param unit * @return true if all went fine within the given time, * otherwise false (in which case the loop is still canceled) * @throws InterruptedException * if the calling thread is interrupted while waiting for the <code>run</code> method to finish. * @throws IllegalStateException if called after shutdown. */ public boolean suspendLoopAndWait(long timeout, TimeUnit unit) throws InterruptedException { suspendLoop(); return taskWrapper.awaitTaskFinish(timeout, unit); } /** * Shuts down this thread loop runner, * attempting to gracefully stop the running task if {@link CancelableRunnable} was provided, * or otherwise letting the currently running loop action finish. * <p> * The <code>ThreadLoopRunner</code> cannot be used any more after this method has been called. * (Then {@link #isDisabled()} will return <code>true</code>, other methods will throw IllegalStateException.) * <p> * The <code>timeout</code> refers to how long this method waits for the task to terminate. * If it terminates before the given timeout, then <code>true</code> is returned, otherwise <code>false</code> * which means that the Runnable action object is still in use and should not be reused later unless it is * re-entrant. * * @param timeout * @param unit * @return true if loop action terminated before the given timeout, or if the loop was not running. * @throws InterruptedException * @throws IllegalStateException if called after shutdown. */ public boolean shutdown(long timeout, TimeUnit unit) throws InterruptedException { if (isDefunct.getAndSet(true)) { throw new IllegalStateException("["+ loopName + "] already disabled"); } loopLock.lock(); try { if (isLoopRunning()) { // prevent further actions from being scheduled loop.cancel(false); loop = null; // cancel current loop action taskWrapper.attemptCancelTask(); runner.shutdown(); logger.finest("["+ loopName + "] task loop has been shut down, will wait for it to fully terminate."); } else { logger.finer("["+ loopName + "] nothing to shut down, task loop was not running."); return true; } } finally { loopLock.unlock(); } // We wait outside of loopLock do avoid deadlocks if the run method also needs loopLock before terminating. boolean ret = runner.awaitTermination(timeout, unit); logger.finer("["+ loopName + "] task " + (ret ? "finished" : "failed to finish") + " within the specified " + timeout + " " + unit.toString().toLowerCase()); // The runner's worker thread#isAlive still returns true right after this shutdown. // We wait a tiny bit, to avoid "Forcibly terminating surviving thread" complaints // from ContainerService's ThreadFactory during component shutdown. // Alternatively we could try to synchronize with ThreadPoolExecutor.terminated() callback. Thread.sleep(2); return ret; } /** * Variation of {@link Runnable} that allows other threads to give a hint to the * {{@link #run()} method that it should terminate. * This is useful mainly with implementations of <code>run()</code> that don't finish immediately. * Note that in Java {@link Thread#stop()} and similar methods are deprecated, and that * the proper way to terminate asynchronously running code is to signal the termination request * via some variable that the thread is supposed to check at convenient points. * <p> * Therefore if your <code>run</code> method takes non-negligible time, you should * <ol> * <li> provide a subclass of this <code>CancelableRunnable</code> as the loop action in * {@link ThreadLoopRunner#ThreadLoopRunner(ThreadFactory, Runnable, Logger)} * <li> implement <code>run()</code> to check at some points whether the flag {@link #shouldTerminate} * has been set (e.g. by {@link ThreadLoopRunner#shutdown} calling {@link CancelableRunnable#cancel()}), * and if so, to return from the run method as quickly as possible, but yet cleaning up. * </ol> */ public abstract static class CancelableRunnable implements Runnable { protected volatile boolean shouldTerminate = false; /** * Either the subclass's run() method evaluates the "shouldTerminate" flag, * or the cancel() method gets overridden so that the subclass can react directly to it. */ public void cancel() { shouldTerminate = true; } } /** * Wrapper of the user-supplied Runnable, which * can inform callers about the execution status of the run() method, or block clients until it finishes. */ private static class TaskWrapper implements Runnable { private final Logger logger; // private boolean extraDebugLogs; /** * The delegate Runnable that gets called from inside our run method. */ private final Runnable delegate; /** * A lock to synchronize on a single task execution. */ private final ReentrantLock runLock; /** * A lock to synchronize on changes of the task loop. */ private final ReentrantLock loopLock; private volatile boolean isRunning; /** * Access must be protected by loopLock. */ private volatile Runnable restartLoopRunnable; TaskWrapper(Runnable delegate, ReentrantLock loopLock, Logger logger) { this.logger = logger; // extraDebugLogs = !delegate.getClass().getName().toLowerCase().contains("alarm"); this.delegate = delegate; runLock = new ReentrantLock(); this.loopLock = loopLock; isRunning = false; } @Override public void run() { runLock.lock(); try { isRunning = true; // if (extraDebugLogs) logger.finest("About to call delegate#run on delegate=" + delegate.getClass().getName()); delegate.run(); // if (extraDebugLogs) logger.finest("Returned from delegate#run"); } catch (Throwable thr) { // If we just let it fly, it will get swallowed by the ScheduledExecutorService // For the time being we just log this exception. // TODO: Should we allow the client to register an error handler and notify it here? logger.log(Level.WARNING, "Uncaught error in scheduled Runnable.", thr); } finally { runLock.unlock(); isRunning = false; } // execute special post-run-task, if any. loopLock.lock(); try { if (restartLoopRunnable != null) { // if (extraDebugLogs) logger.finest("About to call restartLoopRunnable#run"); restartLoopRunnable.run(); // if (extraDebugLogs) logger.finest("Returned from restartLoopRunnable#run"); restartLoopRunnable = null; } } finally { loopLock.unlock(); } } /** * Tests if <code>delegate#run</code> is currently executing. * @return */ boolean isRunning() { return isRunning; } /** * Checks if the delegate Runnable is of subtype {@link CancelableRunnable}, * and if so, calls the <code>cancel()</code> method. */ void attemptCancelTask() { if (delegate instanceof CancelableRunnable) { ((CancelableRunnable) delegate).cancel(); } } /** * Blocks the calling thread if and as long as the <code>delegate#run</code> method executes, * but at most for the given <code>timeout</code>. * @throws InterruptedException */ boolean awaitTaskFinish(long timeout, TimeUnit unit) throws InterruptedException { boolean gotIt = false; try { gotIt = runLock.tryLock(timeout, unit); } finally { if (gotIt) { runLock.unlock(); } } return gotIt; } /** * Allows to restart the loop from inside TaskWrapper#run. */ void restartLoopAfterCurrentTaskFinished(Runnable restartLoopRunnable) { loopLock.lock(); this.restartLoopRunnable = restartLoopRunnable; loopLock.unlock(); // if (extraDebugLogs) { // String msg = "Registered restartLoopRunnable: " + (restartLoopRunnable == null ? "null" : restartLoopRunnable.getClass().getName()); // logger.fine(msg); // } } } }