PeriodicExecutor by Erik van Oosten

Quick download link:
PeriodicExecutor.java

/*
 * Copyright (c) 2008 Erik van Oosten.
 *
 * MIT license.
 *
 */
package nl.grons.util;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**
 * A task executor that will start a task upon request when it has not run for some time.
 *
 * <p>When the task fails 2 times in a row, it will be retried at least 5 minutes later.</p>
 *
 * <p>The task can be started in the background, or in the thread of the caller. The
 * task is never run in parallel.</p>
 *
 * <p>By default the task is started when it has not run for a configurable time.
 * Subclasses may override {@link #nextExecuteTime(long)} to change that.</p>
 *
 * @author Erik van Oosten
 * @see http://day-to-day-stuff.blogspot.com/2008/08/asynchronous-cache-updates.html
 */
public class PeriodicExecutor {

    /** Logging */
    private static final Logger logger = LoggerFactory.getLogger(PeriodicExecutor.class);

    /** Number of times a failing task is retried before the task is delayed. */
    private static final int FAILEDTASK_RETRY_COUNT = 2;
    /** Number of minutes a task is delayed when it has failed too often. Must be strictly positive. */
    private static final long FAILEDTASK_DELAY_MINUTES = 5L;

    private final Object lock;
    /** The task given by the client. */
    private final Runnable task;
    /** Wraps the task. */
    private final PeriodicRunnable periodicRunnable;
    private final long timeoutMillis;
    private long nextExecuteMillis;
    /** True as long no succesful task run has been observed. */
    private boolean initializing;
    /** True when the task is running. */
    private boolean running;
    /** Set to the last exception thrown by the task, unless there has been a succesful run. */
    private RuntimeException initException;
    private int failCount;

    /**
     * Construct.
     *
     * @param timeoutMillis the duration between tasks in milliseconds (>=0)
     *   Value will start the task everytime it is requested. This value is ignored when
     *   {@link #nextExecuteTime(long)} is overrriden.
     * @param task the task (not null)
     *   Note: the toString() method is used in the thread name and in logging. It is
     *   recommended to override the toString() method to return something descriptive.
     */
    public PeriodicExecutor(long timeoutMillis, Runnable task) {
        if (task == null) {
            throw new IllegalArgumentException("Task is null");
        }
        if (timeoutMillis < 0) {
            logger.warn("Timeout is negative!");
        }
        this.lock = new Object();
        this.task = task;
        this.periodicRunnable = new PeriodicRunnable();
        this.timeoutMillis = timeoutMillis;
        nextExecuteMillis = 0L;
        initializing = true;
        initException = null;
        running = false;
        failCount = 0;
    }

    /**
     * Requests a start of the task.
     *
     * <p>When the task has not yet run completely, it will be started in the current thread and
     * this methods blocks until the task is ready.
     * If the task has run to completion earlier, this method will retun inmediately. In addition,
     * when the next execution time has been reached, the task will be started in the background.</p>
     *
     * <p>When the task is already running and was started in the current thread (synchronous),
     * this method blocks until the task has finished. When that execution of the task failed,
     * the task might be restarted.</p>
     *
     * <p>When the taks throws an exception, that exception is kept and rethrown to every caller,
     * even if the task was not triggered by the caller, until the task has run to completion once.
     * After that all exceptions are swallowed (but still logged).</p>
     *
     * @return true when the task was started, false when it is already running or no start is required
     */
    public boolean requestStart() {
        synchronized (lock) {
            return requestStart(initializing);
        }
    }

    /**
     * Requests the start of the task.
     * The task will be started when it has not run yet, or when the next execution time has been reached.
     *
     * <p>When the task is already running and was started with synchronous equal to true, this method
     * blocks (regardless of the value of synchronous in this method call) until the task
     * has finished. When that execution of the task failed, the task might be restarted.</p>
     *
     * <p>When the taks throws an exception, that exception is kept and rethrown to every caller
     * with synchronous equals to true and to each caller with synchronous equals to false while
     * the task was not triggered by the caller. This happens until the task has run to completion
     * once. After that, all exceptions are swallowed (but still logged).</p>
     *
     * @param synchronous true to wait until the task has run, false to run it in the background
     * @return true when the task was started (and has finished when synchronous equals true),
     *         false when it is already running or no start is required
     */
    public boolean requestStart(boolean synchronous) {
        synchronized (lock) {
            boolean start = !running && System.currentTimeMillis() >= nextExecuteMillis;
            if (start) {
                running = true;
                if (synchronous) {
                    // Run synchronously
                    periodicRunnable.run();
                    rethrowInitException();
                } else {
                    // Run asynchronously
                    Thread t = new Thread(periodicRunnable, "PeriodicExecutor for " + task.toString());
                    t.setPriority(Thread.NORM_PRIORITY);
                    t.start();
                }
            } else {
                rethrowInitException();
            }
            return start;
        }
    }

    private void rethrowInitException() {
        if (initException != null) {
            throw initException;
        }
    }

    /**
     * Calculate the next execution time. Can be overriden by subclasses.
     * The default implementation adds the timeout as given in the constructor
     * to the current time.
     *
     * @param currentExecuteTime the currently used execute time (can be 0)
     * @return the next task execution time
     */
    protected long nextExecuteTime(long currentExecuteTime) {
        return System.currentTimeMillis() + timeoutMillis;
    }

    /**
     * Wraps the runnable to enable detection of failures, calculation of next runtime, etc.
     */
    private class PeriodicRunnable implements Runnable {
        /** {@inheritDoc} */
        public void run() {
            String runnableString = task.toString();
            logger.debug("Started execution of runnable task {}", runnableString);
            try {
                task.run();
                synchronized (lock) {
                    initializing = false;
                    initException = null;
                    running = false;
                    failCount = 0;
                    nextExecuteMillis = nextExecuteTime(nextExecuteMillis);
                }
                logger.info("Completed runnable task " + runnableString
                        + " next run scheduled for " + new Date(nextExecuteMillis).toString());

            } catch (Exception e) {
                int previousFailCount;
                boolean nextStartDelayed;
                synchronized (lock) {
                    previousFailCount = failCount;
                    running = false;
                    failCount++;
                    nextStartDelayed = failCount % FAILEDTASK_RETRY_COUNT == 0;
                    if (nextStartDelayed) {
                        nextExecuteMillis = System.currentTimeMillis() + (FAILEDTASK_DELAY_MINUTES * 60L * 1000L);
                    }
                    if (initializing) {
                        initException = new RuntimeException("Runnable task "
                                + runnableString + " (attempt " + previousFailCount
                                + ") failed", e);
                    }
                }
                logger.error("Runnable task " + runnableString + " (attempt " + previousFailCount + ") failed", e);
                if (nextStartDelayed) {
                    logger.warn("Runnable task failed too often, delaying the next run for {} minutes",
                            FAILEDTASK_DELAY_MINUTES);
                }
            }
        }
    }

}