package ca.sqlpower.util;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
/**
* The Scheduler class is a cron-like facility for Java.
*/
public class Scheduler {
private static final Logger logger = Logger.getLogger(Scheduler.class);
/**
* This List contains zero or more ScheduledTask objects sorted by
* nextOccurrence(baseDate) with the first scheduled occurrence
* at position 0 in the list. If two or more scheduled tasks are
* to occur at the same time, their order relative to each other
* is random.
*/
protected static final List schedule;
/**
* This is the base date used for sorting the schedule by next
* occurrence. It is usually the most recently executed event.
*
* <p>This value should only be changed by threads that hold a
* lock on the schedule object.
*/
protected static Date baseDate;
/**
* This thread is created when the Scheduler class is loaded. It
* is responsible for running the ScheduledTask events at the
* correct time and rescheduling them as necessary. It also gets
* notified when a scheduled task is added to or removed from the
* schedule list.
*/
protected static final Thread schedulerThread;
/**
* No-op constructor. This class is meant to be used statically.
*/
protected Scheduler() {}
static {
schedule = new LinkedList();
baseDate = new Date();
schedulerThread = new SchedulerThread();
schedulerThread.start();
}
/**
* Incorporates the given Runnable into the list of scheduled
* tasks, then wakes up the scheduler thread in case it needs to
* reconsider its sleeping time.
*/
public static void scheduleTask(Recurrence recurrence, Runnable task) {
synchronized (schedule) {
Date firstOccurrence = recurrence.nextOccurrence(baseDate);
if (firstOccurrence == null) {
logger.debug("Not scheduling new task because it has no more occurrences");
return;
}
if (firstOccurrence.getTime() < System.currentTimeMillis()) {
logger.debug("Newly scheduled task occurs after base date but before now."
+" Updating base date.");
baseDate = new Date();
}
schedule.add(0, new ScheduledTask(recurrence, task));
Collections.sort(schedule);
schedulerThread.interrupt();
}
}
/**
* Removes the given Runnable task from the schedule. Uses
* straight pointer comparison to find the task to remove, so
* you'll have to pass in a reference that you got from {@link
* #getScheduledTasks()}.
*
* @return The removed task, or null if the given task wasn't
* found in the list.
*/
public static ScheduledTask unscheduleTask(ScheduledTask task) {
synchronized (schedule) {
Iterator it = schedule.iterator();
while (it.hasNext()) {
ScheduledTask st = (ScheduledTask) it.next();
if (st.task == task) {
it.remove();
return task;
}
}
return null;
}
}
public static List getScheduledTasks() {
synchronized (schedule) {
return Collections.unmodifiableList(schedule);
}
}
public static Date getBaseDate() {
return baseDate;
}
protected static class SchedulerThread extends Thread {
public SchedulerThread() {
super("SQLPower Cron");
setDaemon(true);
}
public void run() {
Date wakeup = null;
ScheduledTask nextTask = null;
for (;;) {
try {
synchronized (schedule) {
if (!schedule.isEmpty()) {
nextTask = (ScheduledTask) schedule.get(0);
wakeup = nextTask.recurrence.nextOccurrence(baseDate);
}
}
// determine how long to sleep (0 means sleep until interrupted)
long sleepMillis = 0;
if (wakeup != null) {
sleepMillis = wakeup.getTime() - System.currentTimeMillis();
}
logger.debug("Sleeping "+(wakeup==null?"indefinitely":wakeup.toString()));
try {
join(sleepMillis); // not using Thread.sleep() because 0 means forever
} catch (InterruptedException e) {
logger.debug("Received an interrupt while sleeping");
}
// remember when we woke up
wakeup = new Date();
// now that we're awake, we will perform all tasks that were due
// at the actual wakeup time
// XXX: schedule remains locked during task execution!
synchronized (schedule) {
if (!schedule.isEmpty()) {
nextTask = (ScheduledTask) schedule.get(0);
Date nextTime = nextTask.recurrence.nextOccurrence();
while (nextTime != null
&& nextTime.getTime() <= System.currentTimeMillis()) {
try {
logger.debug("Starting to run scheduled task");
nextTask.task.run();
} catch (Exception e) {
logger.error("Scheduled task threw an exception", e);
}
baseDate = nextTime;
Collections.sort(schedule);
nextTask = (ScheduledTask) schedule.get(0);
nextTime = nextTask.recurrence.nextOccurrence();
}
}
}
} catch (Exception e) {
logger.error("Unexpected exception in scheduler thread", e);
return; // XXX: should remove this in production!
} finally {
logger.error("SchedulerThread.run() is exiting!");
}
}
}
}
/**
* A container for the objects that represent a scheduled task.
* Comparisons of objects of this type are based on the
* recurrence's nextOccurrence() after the Scheduler's current
* base date.
*/
public static class ScheduledTask {
public Recurrence recurrence;
public Runnable task;
public ScheduledTask(Recurrence recurrence, Runnable task) {
this.recurrence = recurrence;
this.task = task;
}
/**
* Compares this ScheduledTask to the other given task. The
* comparison is based on the return value of the recurrence's
* nextOccurrence(Scheduler.baseDate). If other is not an instance of
* ScheduledTask, you will get a ClassCastException.
*
* <p>A null nextOccurrence (meaning the task has no more
* scheduled occurrences) sorts to the end (infinitely far in
* the future). This helps with pruning the schedule because
* after sorting all the expired tasks will be grouped at the
* end.
*/
public int compareTo(Object other) {
ScheduledTask otherTask = (ScheduledTask) other;
Date thisNextOccurrence = this.recurrence.nextOccurrence(baseDate);
Date otherNextOccurrence = otherTask.recurrence.nextOccurrence(baseDate);
if (thisNextOccurrence == null && otherNextOccurrence == null) {
return 0;
} else if (thisNextOccurrence == null) {
return 1;
} else if (otherNextOccurrence == null) {
return -1;
} else {
return (int) (thisNextOccurrence.getTime() - otherNextOccurrence.getTime());
}
}
/**
* Tells if two scheduled tasks will occur next at exactly the
* same time. Warning: this method was implemented so equals
* could be consistent with compareTo, but it is not all that
* useful in reality. Two different scheduled tasks will be
* considered equal if their next occurrences coincide.
*/
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
ScheduledTask otherTask = (ScheduledTask) other;
Date thisNextOccurrence = this.recurrence.nextOccurrence(baseDate);
Date otherNextOccurrence = otherTask.recurrence.nextOccurrence(baseDate);
if (thisNextOccurrence == null && otherNextOccurrence == null) {
return true;
} else if (thisNextOccurrence == null) {
return false;
} else if (otherNextOccurrence == null) {
return false;
} else {
return thisNextOccurrence.getTime() == otherNextOccurrence.getTime();
}
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + this.recurrence.nextOccurrence(baseDate).hashCode();
return result;
}
}
}