|
|
package org.openslx.taskmanager.api;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.openslx.taskmanager.api.TaskStatus.StatusCode;
import com.google.gson.annotations.Expose;
public abstract class AbstractTask implements Runnable
{
private static final long RELEASE_DELAY = 5l * 60l * 1000l;
private static final Logger LOG = Logger.getLogger( AbstractTask.class );
/*
* To be set from task invocation (json data)
*/
/**
* The id of the task instance.
*/
@Expose
private volatile String id = null;
/**
* Parent task. This task won't be started as long as the parent task currently
* waiting for execution or is being executed. Otherwise this task is available for execution.
* Note that MAX_INSTANCES is still being taken into account. Set to null to ignore.
*/
@Expose
private volatile String parentTask = null;
/**
* If the parent task failed to execute, don't run this task and fail immediately.
*/
@Expose
private volatile boolean failOnParentFail = true;
/*
* Variables we're working with - these should never be set from incoming (json) data
*/
/**
* Maximum age of a task, if it's not freed explicitly.
*/
private static final long MAX_TASK_AGE = 24l * 3600l * 1000l;
/**
* timeMillis when this task will be removed. This will be set automatically to something
* reasonable like 24 hours, once the job has finished. This is to prevent clogging the task
* manager with finished jobs over time. Note that you can explicitly remove a job using the
* "release" command.
*/
private volatile long removalDeadline = System.currentTimeMillis() + MAX_TASK_AGE;
/**
* True if init() has been called. Task will not start if this is false.
*/
private volatile boolean initDone = false;
/**
* Status of Task
*/
private volatile TaskStatus status = TaskStatus.ts_waiting;
/**
* Reference to parent task
*/
private volatile AbstractTask parent = null;
/**
* Reference to "owner" so we can tell it to check for more
* work when we're done.
*/
private volatile FinishCallback finishCallback = null;
/**
* Set to true as soon as we try to start this task using the thread pool executor.
*/
private volatile boolean triedToStart = false;
/**
* Default constructor which should not be overridden
*/
public AbstractTask()
{
this.id = UUID.randomUUID().toString();
}
/*
* Overridable methods
*/
/**
* Initialize the task; method used by the {@link Taskmanager}.
* Put your own initialization code in initTask()
*/
public final boolean init( AbstractTask parent, FinishCallback finishCallback )
{
if ( this.initDone ) {
LOG.fatal( "init() called twice on " + this.getClass().getSimpleName() );
System.exit( 1 );
}
if ( this.parentTask != null && this.parentTask.isEmpty() )
this.parentTask = null;
if ( this.parentTask != null && parent == null ) {
if ( this.failOnParentFail ) {
this.status = new TaskStatus( StatusCode.PARENT_FAILED, this.id );
return false;
}
this.parentTask = null;
}
if ( this.parentTask == null && parent != null )
parent = null;
this.finishCallback = finishCallback;
this.parent = parent;
this.status = new TaskStatus( StatusCode.TASK_WAITING, this.id );
this.initDone = true;
boolean ret;
try {
ret = this.initTask();
} catch ( Throwable t ) {
ret = false;
}
if ( !ret ) {
this.status.statusCode = StatusCode.TASK_ERROR;
}
return ret;
}
/**
* Your own initialization code. This is run synchronously within the network
* handling thread, so do NOT put anything here that might take longer than a few milliseconds!
* You should usually only validate the input to the task here, and return false if it is
* invalid.
*
* @return - true if the task should be executed by the scheduler (ie init was successful)
* - false if init was not successful, or you don't need to do any more processing
*/
protected abstract boolean initTask();
/**
* This is where you put your huge work stuff that takes ages.
*/
protected abstract boolean execute();
/**
* This is called when a client requests the status of this task. In case you
* want to return complex structures like Lists, which are not thread safe, you
* might want to keep that list outside the status class you return, and only
* create a copy of it for the status class in this function.
* If you only return more or less atomic data, you don't need to override
* this function
*/
protected void updateStatus()
{
}
/*
* Final methods
*/
/**
* Get id of parent task.
*
* @return id of parent task, null if no parent set
*
*/
public final String getParentTaskId()
{
return this.parentTask;
}
/**
* Set the custom status data object to be returned on status requests.
* For simple tasks it should be sufficient to set this once before executing
* the task starts, and then just update fields in that class while the
* task is running. For cases where you have complicated data structures or
* multiple values that need to stay in sync you could create a new instance
* every time, fill it with values, and then call this method.
*
* @param obj the object containing you specific task data
*/
protected final void setStatusObject( Object obj )
{
status.setStatusObject( obj );
}
/**
* Get current status of task.
*/
public final TaskStatus getStatus()
{
if ( this.initDone && this.parent != null ) {
final StatusCode parentStatus = parent.getStatusCode();
switch ( parentStatus ) {
case DUPLICATE_ID:
case NO_SUCH_INSTANCE:
case NO_SUCH_TASK:
case NO_SUCH_CONSTRUCTOR:
case PARENT_FAILED:
case TASK_ERROR:
if ( this.failOnParentFail ) {
this.status.statusCode = StatusCode.PARENT_FAILED;
LOG.debug( "Parent " + this.parentTask + " of " + this.id + " failed." );
}
this.parentTask = null;
break;
default:
break;
}
}
return this.status;
}
/**
* Get status code if task.
*
* @return
*/
public final StatusCode getStatusCode()
{
return getStatus().getStatusCode();
}
/**
* Get id of task
*/
public final String getId()
{
return this.id;
}
/**
* Getter for failOnParentTask
*/
public final boolean getFailOnParentFail()
{
return this.failOnParentFail;
}
/**
* Release the task: If the task is still pending/running, it will be removed from the task
* manager as soon as it finishes. So you can't query for its result later. If the task has
* already finished (either successful or failed), it will be removed (almost) immediately.
*/
public final void release()
{
this.removalDeadline = Math.min( this.removalDeadline, System.currentTimeMillis() + RELEASE_DELAY );
}
/**
* Can this task be started? This checks all the conditions:
* - Has this task been initialized yet?
* - Is it actually waiting for execution?
* - Does it depend on a parent task?
* -- If so, did the parent finish?
* -- Or did it fail?
* --- If so, should this task only run if it didn't fail?
*
* @return true iff this task can be started
*/
public final boolean canStart()
{
if ( this.triedToStart || !this.initDone || this.id == null || this.getStatus().getStatusCode() != StatusCode.TASK_WAITING ) {
return false;
}
if ( this.parent == null )
return true;
return parent.getStatusCode() != StatusCode.TASK_WAITING && parent.getStatusCode() != StatusCode.TASK_PROCESSING;
}
/**
* Mark this task as being started to prevent a race condition where
* the task would be submitted to the thread pool more than once.
*/
public final void markAsStarting()
{
this.triedToStart = true;
}
/**
* Checks whether this task can be removed from the task manager.
* A task can be removed if it is not executing or waiting for execution, and
* its removal deadline has been reached.
*
* @return true if it can be released
*/
public final boolean canBeReleased()
{
if ( this.status.statusCode == StatusCode.TASK_WAITING || this.status.statusCode == StatusCode.TASK_PROCESSING )
return false;
return this.removalDeadline != 0 && System.currentTimeMillis() > this.removalDeadline;
}
/**
* Check whether the given task is finished, i.e. either failed or succeeded,
* or couldn't be started because its parent failed,
* but is not running or still waiting for execution.
*
* @return true if task finished running or was not able to start
*/
public final boolean isFinished()
{
StatusCode sc = getStatusCode();
return sc != StatusCode.TASK_WAITING && sc != StatusCode.TASK_PROCESSING;
}
/**
* Execute the task, wrapped in some sanity checks.
*/
@Override
public final void run()
{
synchronized ( this ) {
if ( this.status.statusCode != StatusCode.TASK_WAITING )
throw new RuntimeException( "Tried to launch task " + this.getClass().getSimpleName() + " twice!" );
if ( !this.initDone )
throw new RuntimeException( "Tried to launch " + this.getClass().getSimpleName() + " without initializing it!" );
this.status.statusCode = StatusCode.TASK_PROCESSING;
}
boolean ret;
try {
ret = execute();
} catch ( Throwable t ) {
LOG.warn( "Task " + this.getClass().getSimpleName() + " failed with uncaught exception", t );
ret = false;
}
if ( ret ) {
this.status.statusCode = StatusCode.TASK_FINISHED;
} else if ( this.status.statusCode == StatusCode.TASK_CANCELLING ) {
this.status.statusCode = StatusCode.TASK_CANCELLED;
} else {
this.status.statusCode = StatusCode.TASK_ERROR;
}
LOG.debug( "Finished task " + this.getClass().getSimpleName() + ": " + this.status.statusCode.toString() + " (" + this.id + ")" );
if ( this.finishCallback != null )
this.finishCallback.taskFinished();
}
}
|