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);
}
}
}
}
}