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 = 10l * 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(); } }