package winterwell.utils.threads;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import winterwell.utils.StrUtils;
import winterwell.utils.TimeOut;
import winterwell.utils.Utils;
import winterwell.utils.io.FileUtils;
import winterwell.utils.io.SysOutCollectorStream;
import winterwell.utils.time.Dt;
import winterwell.utils.time.Time;
/**
*
* Provides {@link #cancel()}. When run, sets the running thread's name for
* debugging info.
*
* @author daniel
*
* @param <V>
* return type
*/
public abstract class ATask<V> implements Callable<V>, IProgress {
/**
* The enum ordering follows the lifecycle, so you can use < to compare
* states. E.g.
* <code>while(status.ordinal() < QStatus.STOPPING.ordinal()) {do stuff}</code>
*/
public static enum QStatus {
/** Initial putting-the-task-together state. */ NOT_SUBMITTED,
/** Start requested. */ WAITING,
/** Doing It's Thang! */ RUNNING,
/** Stop requested but not yet processed */ STOPPING,
/** Stopped: properly*/ DONE,
/** Stopped with error */ ERROR,
/** Stopped: user cancelled */ CANCELLED;
/**
* @return true for the finished states of DONE, ERROR and CANCELLED.<br>
* false for everything earlier (including STOPPING).
*/
public boolean isFinished() {
return ordinal() >= DONE.ordinal();
}
}
boolean captureStdOut;
/**
* Time the task stopped running. null if not run yet.
*/
private Time end;
/**
* If >0, we will set a TimeOut
*/
private long maxTime;
private final String name;
private V output;
transient TaskRunner runner;
/**
* Time the task started running. null if not started yet.
*/
private Time start;
private volatile QStatus status = QStatus.NOT_SUBMITTED;
private transient SysOutCollectorStream sysOut;
private transient Thread thread;
public ATask() {
this(null);
}
/**
*
* @param name
* This will be used as the thread name whilst running.
*/
public ATask(String name) {
this.name = name;
}
/**
* Wraps {@link #run()} with timing, thread-naming, and capturing std-out
* (if set to do so)
*/
@Override
public final V call() throws Exception {
TimeOut timeOut = null;
try {
if (status == QStatus.CANCELLED)
throw new CancellationException();
start = new Time();
// set a timer running
// - can lead to InterruptedExceptions
if (maxTime > 0) {
timeOut = new TimeOut(maxTime);
}
// assert runner != null;
status = QStatus.RUNNING;
thread = Thread.currentThread();
// Set the thread name (but keep it short)
// This will always be reset to Done: or Error: by the end of the
// method call
thread.setName(StrUtils.ellipsize(name == null ? toString() : name,
32));
if (captureStdOut) {
sysOut = new SysOutCollectorStream();
}
// run!
output = run();
end = new Time();
status = QStatus.DONE;
thread.setName(StrUtils.ellipsize("Done: " + thread.getName(), 32));
return output;
} catch (Throwable e) {
status = QStatus.ERROR;
if (runner != null) {
runner.report(this, e);
}
thread.setName(StrUtils.ellipsize("Error: " + thread.getName(), 32));
// There's not much point throwing an exception from within an
// executor
// - but it's useful in debugging.
throw Utils.runtime(e);
} finally {
if (timeOut != null) {
timeOut.cancel();
}
if (runner != null) {
runner.done(this);
}
// drop references
runner = null;
thread = null;
FileUtils.close(sysOut);
}
}
/**
* Cancel this task - it will be skipped over instead of running. Should
* only be called on tasks which have status == WAITING (or status ==
* CANCELLED in which case it does nothing).
* <p>
* The effects of cancelling a running task are undefined. They depend on
* {@link #cancel2_running()}
*
* @throws IllegalStateException
*/
public void cancel() throws IllegalStateException {
switch (status) {
case CANCELLED:
return; // no-op
case WAITING:
break;
case NOT_SUBMITTED:
break;
case DONE:
return; // no-op
case RUNNING:
// what to do?
cancel2_running();
}
if (runner != null) {
runner.done(this);
}
status = QStatus.CANCELLED;
}
protected void cancel2_running() throws IllegalStateException {
// the finally clause should take care of setting thread=null
int cnt = 0;
while (thread != null) {
thread.interrupt();
cnt++;
if (cnt == 10) {
// No joy? then be more aggressive
thread.stop();
break;
}
Utils.sleep(100);
}
}
/**
* Implement a task-specific version of equals() and hashCode() if you want
* to avoid duplicate tasks.
*/
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
/**
* Get intermediate results. Override to do anything - the default returns
* null.
*
* @return
* @see #getProgress()
*/
public V getIntermediateOutput() {
return null;
}
public String getName() {
return name;
}
/**
* Only valid if status == DONE
*
* @return
*/
public V getOutput() {
assert status == QStatus.DONE : status;
return output;
}
@Override
public double[] getProgress() {
return null;
}
public Dt getRunningTime() {
if (start == null)
return Dt.ZERO();
return start.dt(end == null ? new Time() : end);
}
public QStatus getStatus() {
return status;
}
/**
* Output sent to standard out via System.out during this Task's run. Only
* valid if {@link #setCaptureStdOut(boolean)} was set to true. Otherwise
* will return null.
*
* @see #isCapturingStdOut()
*/
public String getStdOut() {
// assert status == QStatus.DONE || status == QStatus.RUNNING : status;
if (sysOut == null)
return null;
return sysOut.toString();
}
/**
* Implement a task-specific version of equals() and hashCode() if you want
* to avoid duplicate tasks.
*/
@Override
public int hashCode() {
return super.hashCode();
}
/**
* TODO interrupt the running thread
*
* @throws IllegalStateException
*/
public void interrupt() throws IllegalStateException {
if (status == QStatus.CANCELLED)
return; // no-op
if (status != QStatus.RUNNING)
throw new IllegalStateException(status + " " + this);
// Test: what does this do? inc to the queue
thread.interrupt();
}
public boolean isCapturingStdOut() {
return captureStdOut;
}
/**
* Do whatever it is this task does!
*
* @return
* @throws Exception
*/
protected abstract V run() throws Exception;
public void setCaptureStdOut(boolean captureStdOut) {
this.captureStdOut = captureStdOut;
// Were we capturing? Should we stop?
if (!captureStdOut && sysOut != null) {
FileUtils.close(sysOut);
}
// Already running? Should we start capturing std-out from now?
if (captureStdOut && getStatus() == QStatus.RUNNING && sysOut == null) {
sysOut = new SysOutCollectorStream();
}
}
/**
* @param null for no timeout. This will use a {@link TimeOut} to interrupt
* the thread.
*/
public void setMaxTime(Dt maxTime) {
this.maxTime = maxTime == null ? 0 : maxTime.getMillisecs();
}
void setTaskRunner(TaskRunner runner) {
assert this.runner == null : "Task " + this
+ " already assigned to runner " + this.runner;
this.runner = runner;
status = QStatus.WAITING;
}
@Override
public String toString() {
if (name != null)
return getClass().getName() + "[" + name + "]";
// have a predictable name for TaskRunnerWithStats
return getClass().getName();
}
}