/**
*
*/
package winterwell.utils.threads;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import winterwell.utils.reporting.Log;
import winterwell.utils.time.StopWatch;
/**
* Run tasks in an offline asynchronous manner. Allows some management of the
* queue.
*
* Intended usage: for handling relatively few relatively slow tasks.
*
* @author daniel
* @testedby {@link TaskRunnerTest}
*/
public class TaskRunner {
/** default for arbitrary tasks */
private static TaskRunner dflt;
/**
* default for high-priority tasks -- which must be fast!
*/
private static TaskRunner highPriority;
/**
* how many tasks to keep around in done
*/
private static final int HISTORY = 6;
private static final int NUM_THREADS_FOR_DEFAULT = 6;
public static TaskRunner getDefault() {
if (dflt == null) {
dflt = new TaskRunner(NUM_THREADS_FOR_DEFAULT);
}
return dflt;
}
/**
* This is a default task-runner for fast high-priority tasks. E.g. tasks
* which cause the user-interface to lag. Do NOT submit slow tasks!
*/
public static TaskRunner getFastDefault() {
if (highPriority == null) {
highPriority = new TaskRunner(NUM_THREADS_FOR_DEFAULT);
}
return highPriority;
}
public static void setDefault(TaskRunner dflt) {
TaskRunner.dflt = dflt;
}
/**
* Includes cancelled tasks
*/
private final Queue<ATask> done = new ConcurrentLinkedQueue<ATask>();
private final ExecutorService exec;
/**
* Includes running tasks
*/
private final Queue<ATask> todo = new ConcurrentLinkedQueue<ATask>();
/**
* Create a 2-thread TaskRunner
*/
public TaskRunner() {
this(2);
}
/**
* A TaskRunner which uses the current thread
*
* @param thisThread
* Must be true
*/
public TaskRunner(boolean thisThread) {
assert thisThread;
exec = new AbstractExecutorService() {
AtomicInteger running = new AtomicInteger();
@Override
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
timeout = unit.toMillis(timeout);
StopWatch sw = new StopWatch();
while (running.get() > 0) {
if (sw.getTime() >= timeout)
return false;
}
return true;
}
@Override
public void execute(Runnable command) {
running.incrementAndGet();
command.run();
running.decrementAndGet();
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public void shutdown() {
}
@Override
public List<Runnable> shutdownNow() {
return Collections.emptyList();
}
};
}
public TaskRunner(int numThreads) {
assert numThreads > 0;
exec = Executors.newFixedThreadPool(numThreads);
}
/**
* Clean up any references to task. Called by the task on success, failure
* or cancellation.
*
* @param task
*/
protected synchronized void done(ATask task) {
todo.remove(task);
done.add(task);
if (done.size() >= HISTORY) {
done.remove();
}
}
/**
* Most recent finished tasks. Includes cancelled tasks.
*/
public Collection<ATask> getDone() {
return done;
}
public int getQueueSize() {
return todo.size();
}
/**
* pending and currently-running tasks
*
* @return
*/
public Collection<ATask> getTodo() {
return todo;
}
/**
* Is the task running or queued. Task equality is determined by equals() in
* the usual way.
*
* @param task
* @return true if in {@link #todo}
*/
public boolean hasTask(ATask task) {
return todo.contains(task);
}
/**
* Called when a task throws an exception. Reports it to {@link Log}. Can be
* over-ridden.
* <p>
* Note that the {@link Future} object will also still throw an exception.
*
* @param noisyRunnable
* @param e
*/
public void report(Object runnableOrCallable, Throwable e) {
Log.report(e);
}
/**
* Submit a task for processing. Throws an @link
* {@link IllegalArgumentException} if an equivalent task is already running
* or queued. (unless overridden, an equivalent task is an identical one).
* TODO: Possibly introduce a trySubmit() that always succeeds
*
* @param task
* @return
*/
public synchronized Future submit(ATask task) {
if (todo.contains(task))
throw new IllegalArgumentException(
"An equivalent task is already running/queued");
task.setTaskRunner(this);
todo.add(task);
Future f = exec.submit(task);
return f;
}
}