package org.jboss.jbossts.qa.junit;
import java.util.SortedSet;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.Iterator;
/**
* Task manager which ensures that processes created during unit tests are destroyed when they
* do no complete after a suitable timeout.
*/
public class TaskReaper
{
// api methods
// n.b all internal methods synchronize on the value in the reaperLock field when they need
// exclusive access to the internal state. note that the api methods are not synchronized but
// they should nto be called concurrently.
/**
* insert a task into the list of managed tasks
* @param task the task to be inserted
* @param absoluteTimeout the absolute system time measured in milliseconds from the epoch at which
* the task's process should be destroyed if it has not been removed from the list by then
*/
public void insert(TaskImpl task, long absoluteTimeout)
{
synchronized(reaperLock) {
if (shutdown) {
throw new RuntimeException("invalid call to TaskReaper.insert after shutdown");
}
TaskReapable reapable = new TaskReapable(task, absoluteTimeout);
reapableMap.put(task, reapable);
taskList.add(reapable);
// notify the reaper thread
reaperLock.notify();
}
}
/**
* remove a task from the list of managed tasks
* @param task the task to be removed
* @return true if the task was present in the list and was removed before a timeout caused its
* process to be destroyed otherwise false
*/
public boolean remove(TaskImpl task)
{
synchronized(reaperLock) {
TaskReapable reapable = reapableMap.get(task);
if (reapable != null) {
taskList.remove(reapable);
// notify the reaper thread
reaperLock.notify();
return true;
}
}
return false;
}
/**
* check if there are any tasks in the list
* @return true if the list is empty otherwise false
*/
public boolean allClear()
{
synchronized (reaperLock) {
return taskList.isEmpty();
}
}
/**
* remove any remaining tasks from the list, destroying their process, and return a count of the
* number of tasks which have failed to exit cleanly. this count includes tasks which have been killed
* because of timeouts as well as those destroyed under the call to clear().
* @return a count of how many tasks exited abnormally.
*/
public int clear()
{
int returnCount;
synchronized(reaperLock) {
// set the absolute timeout of every task to zero then wake up the reaper and wait for
// the list to empty
SortedSet<TaskReapable> copyOfTaskList = new TreeSet<TaskReapable>(taskList);
Iterator<TaskReapable> iterator = copyOfTaskList.iterator();
while (iterator.hasNext()) {
TaskReapable reapable = iterator.next();
// absoluteTimeout value is part of the ordering, so we screw up the sorted set if we
// modify the obj in situ. remove, modify and reinsert to work around this.
taskList.remove(reapable);
reapable.absoluteTimeout = 0;
taskList.add(reapable);
}
reaperLock.notify();
// ok now wait until all the tasks have gone
// n.b this relies upon the caller not inserting new tasks while the clear operation is in progress!
while (!taskList.isEmpty()) {
try {
reaperLock.wait();
} catch (InterruptedException e) {
// ignore -- we should never be interrupted here
}
}
returnCount = invalidCount;
invalidCount = 0;
}
return returnCount;
}
/**
* shut down the task manager
* @param immediate if true then shut down without destroying any task in the current list otherwise
* atempt to destroy all pending tasks.
*/
public void shutdown(boolean immediate)
{
// unset the current reaper instance
clearTheReaper(this);
// now ensure that any tasks it had pending are removed or time out
synchronized (reaperLock) {
shutdown = false;
// setting shutdownWait to false makes the reaper thread exit without clearing the list
// setting it true makes it exit once all tasks have been destroyed
if (immediate) {
shutdownWait = false;
} else {
shutdownWait = true;
// set the absolute timeout of every task to zero then wake up the reaper and wait for
// the list to empty
Iterator<TaskReapable> iterator = taskList.iterator();
while (iterator.hasNext()) {
TaskReapable reapable = iterator.next();
reapable.absoluteTimeout = 0;
}
}
// notify so that the reaper thread wakes up
reaperLock.notify();
// we don't get out of here until the reaper thread has exited
while (!threadShutdown) {
try {
reaperLock.wait();
} catch (InterruptedException e) {
// ignore -- we should never be interrupted here
}
}
}
}
/**
* obtain a handle on the currently active reaper, creating a new one if there is no reaper active
* @return the reaper
*/
public static synchronized TaskReaper getReaper()
{
if (theReaper == null) {
createReaper();
}
return theReaper;
}
/**
* reset the current reaper instance to null. this is called from the current reaper instanmce's shutdown
* method to reset the current handle to null.
*/
private static synchronized void clearTheReaper(TaskReaper theReaperReaped)
{
// if the current reaper still identifies the one we just shutdown then reset it to null
if (theReaper == theReaperReaped) {
theReaper = null;
}
}
// implementation methods and state
// package public access only for use by TaskReaperThread
/**
* entry point for the task reaper thread to detect timed out tasks in the background. this should not
* be called anywhere except in TaskReaperThread.run
*/
void check()
{
synchronized(reaperLock) {
while (!shutdown || shutdownWait) {
if (taskList.isEmpty()) {
// wait as long as we need to
try {
reaperLock.wait();
} catch (InterruptedException e) {
// ignore -- we should never be interrupted here
}
} else {
TaskReapable first = taskList.first();
long absoluteTime = System.currentTimeMillis();
long firstAbsoluteTime = first.getAbsoluteTimeout();
if (absoluteTime < firstAbsoluteTime) {
// use difference to limit wait
try {
reaperLock.wait(firstAbsoluteTime - absoluteTime);
} catch (InterruptedException e) {
// ignore -- we should never be interrupted here
}
} else {
// we have a task to kill so kill it, wait a brief interval so we don't hog
// the cpu and then loop to see if there are more to kill
if (timeout(first)) {
invalidCount++;
}
// notify here in case a thread was trying to modify the list while we were doing
// the timeout
reaperLock.notify();
try {
reaperLock.wait(1);
} catch (InterruptedException e) {
// ignore -- we should never be interrupted here
}
}
}
}
threadShutdown = true;
// notify here so we wakeup the thread which initiated the shutdown
reaperLock.notify();
}
}
private static TaskReaper theReaper = null;
private SortedSet<TaskReapable> taskList;
private HashMap<TaskImpl, TaskReapable> reapableMap;
private int invalidCount;
private boolean shutdown;
private boolean shutdownWait;
private boolean threadShutdown;
private Object reaperLock;
private TaskReaperThread reaperThread;
private TaskReaper()
{
taskList = new TreeSet<TaskReapable>();
reapableMap = new HashMap<TaskImpl, TaskReapable>();
invalidCount = 0;
shutdown = false;
shutdownWait = false;
threadShutdown = false;
reaperLock = new Object();
reaperThread = new TaskReaperThread(this);
reaperThread.start();
}
/**
* start the task manager
*/
private static void createReaper()
{
theReaper = new TaskReaper();
}
/**
* destroy a timed out task and remove it from the task list. n.b. this must be called when
* synchronized on the reaper lock
* @param reapable the task to be destroyed
* @return true if the task exited invalidly otherwise false
*/
private boolean timeout(TaskReapable reapable)
{
TaskImpl task = reapable.getTask();
reapableMap.remove(task);
taskList.remove(reapable);
return reapable.getTask().timeout();
}
/**
* wrapper which associates a task with its absoulte timeout and provides a comparator which allows
* tasks to be sorted in order of absolute timeout
*/
private static class TaskReapable implements Comparable<TaskReapable>
{
public TaskReapable(TaskImpl task, long absoluteTimeout)
{
long now = System.currentTimeMillis();
this.absoluteTimeout = now + absoluteTimeout;
this.task = task;
}
public long getAbsoluteTimeout()
{
return absoluteTimeout;
}
public TaskImpl getTask() {
return task;
}
private long absoluteTimeout;
private TaskImpl task;
public int compareTo(TaskReapable o) {
if (this == o) {
return 0;
}
if (absoluteTimeout < o.absoluteTimeout) {
return -1;
} else if (absoluteTimeout > o.absoluteTimeout) {
return 1;
} else {
// try to sort using hash codes
int h = hashCode();
int oh = o.hashCode();
if (h < oh) {
return -1;
} else {
return 1;
}
}
}
}
private static class TaskReaperThread extends Thread
{
public TaskReaperThread(TaskReaper reaper)
{
this.reaper = reaper;
}
public void run()
{
reaper.check();
}
private TaskReaper reaper;
}
}