/*==========================================================================*\
| $Id: WorkerThread.java,v 1.9 2011/12/25 21:18:24 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2009-2011 Virginia Tech
|
| This file is part of Web-CAT.
|
| Web-CAT is free software; you can redistribute it and/or modify
| it under the terms of the GNU Affero General Public License as published
| by the Free Software Foundation; either version 3 of the License, or
| (at your option) any later version.
|
| Web-CAT is distributed in the hope that it will be useful,
| but WITHOUT ANY WARRANTY; without even the implied warranty of
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
| GNU General Public License for more details.
|
| You should have received a copy of the GNU Affero General Public License
| along with Web-CAT; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/
package org.webcat.jobqueue;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.apache.log4j.Logger;
import org.jfree.util.Log;
import org.webcat.core.Application;
import org.webcat.woextensions.WCEC;
import org.webcat.woextensions.WCFetchSpecification;
import com.webobjects.eoaccess.EOGeneralAdaptorException;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.foundation.NSArray;
import er.extensions.eof.ERXQ;
import er.extensions.eof.ERXS;
//-------------------------------------------------------------------------
/**
* Implements a single worker thread on a single host, operating on a
* shared database-backed queue of jobs represented as {@link JobBase}
* subclass objects.
*
* @param <Job> The subclass of {@link JobBase} that this worker thread
* works on.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.9 $, $Date: 2011/12/25 21:18:24 $
*/
public abstract class WorkerThread<Job extends JobBase>
extends Thread
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new object.
* @param queueEntity The name of the entity representing the job
* queue for this worker thread.
*/
public WorkerThread(String queueEntity)
{
setName(this.getClass().getSimpleName() + "-" + getId());
EOEditingContext myec = localContext();
descriptor = new ManagedWorkerDescriptor(
WorkerDescriptor.registerWorker(
myec,
HostDescriptor.currentHost(myec),
QueueDescriptor.descriptorFor(myec, queueEntity),
this));
}
// ----------------------------------------------------------
/**
* Access the queue descriptor for this worker's job queue.
* @return The queue descriptor
*/
public ManagedQueueDescriptor queueDescriptor()
{
if (queueDescriptor == null)
{
queueDescriptor = new ManagedQueueDescriptor(descriptor.queue());
}
return queueDescriptor;
}
// ----------------------------------------------------------
/**
* Access the host descriptor for this worker's host.
* @return The host descriptor
*/
public ManagedHostDescriptor hostDescriptor()
{
if (hostDescriptor == null)
{
hostDescriptor = new ManagedHostDescriptor(descriptor.host());
}
return hostDescriptor;
}
// ----------------------------------------------------------
/**
* Access the descriptor for this worker.
* @return The worker descriptor
*/
public ManagedWorkerDescriptor descriptor()
{
return descriptor;
}
//~ Public Methods ........................................................
// ----------------------------------------------------------
/**
* The actual thread of execution, which cannot be overridden.
*/
@SuppressWarnings("unchecked")
public final void run()
{
// Make sure application is fully initialized before running.
Application.waitForInitializationToComplete();
logDebug("started");
while (true)
{
try
{
killCancelledJobs();
waitForAvailableJob();
long jobStartTime = System.currentTimeMillis();
boolean jobFailed = false;
try
{
processJob();
}
catch (Exception e)
{
localContext().revert();
jobFailed = true;
currentJob.setIsReady(false);
currentJob.setWorkerRelationship(null);
resetJob();
sendJobSuspensionNotification(e);
currentJob = null;
// TODO check for optimistic locking failures?
localContext().saveChanges();
}
// If the job set its own state back to not-ready, consider
// it as failed so that we don't delete it and can come back
// to it later.
if (currentJob != null && !currentJob.isReady())
{
jobFailed = true;
currentJob.setWorkerRelationship(null);
currentJob = null;
// TODO check for optimistic locking failures?
localContext().saveChanges();
}
if (!jobFailed)
{
long now = System.currentTimeMillis();
long jobDuration = now - jobStartTime;
long jobWait = now - currentJob.enqueueTime().getTime();
boolean wasCancelled = currentJob.isCancelled();
currentJob.delete();
try
{
localContext().saveChanges();
currentJob = null;
// Update the wait statistics.
if (!wasCancelled)
{
queueDescriptor().addCompletedJobStats(
jobDuration, jobWait);
}
}
catch (Exception e)
{
Number jobId = currentJob.id();
// Refresh the editing context.
renewContext();
// Get a local instance of the job with the same id.
NSArray<Job> results =
EOUtilities.objectsMatchingKeyAndValue(
localContext(),
queueDescriptor().jobEntityName(),
"id", jobId.intValue());
if (results != null && results.count() > 0)
{
currentJob = results.objectAtIndex(0);
}
else
{
currentJob = null;
}
}
}
}
catch (Exception e)
{
// FIXME what should we do here?
log.error("Exception in worker thread:", e);
renewContext();
}
}
}
//~ Protected Methods .....................................................
// ----------------------------------------------------------
/**
* Subclasses should implement this method to process the
* {@link #currentJob()}. All other work involving finding the next
* job, managing the job queue, and so on is already implemented in
* this abstract base class. If this method throws any exceptions,
* they will force the current job to be suspended (paused) and then
* the {@link #sendJobSuspensionNotification()} method will be
* invoked.
*
* @throws Exception if an exception occurred
*/
protected abstract void processJob() throws Exception;
// ----------------------------------------------------------
/**
* Resets the state of the job when it is suspended due to an exception.
* Subclasses can override this method to perform any additional
* modification of specific job attributes.
*/
protected void resetJob()
{
// Default implementation does nothing.
}
// ----------------------------------------------------------
/**
* Called to handle a cancellation request for the job owned by this
* thread. The default behavior simply sets the isCancelled flag of the
* thread to true so that it can be polled in the processJob method, but
* subclasses may override this to provide their own cleanup logic if
* necessary.
*
* Subclasses that override this method should always call the super
* method first.
*/
protected synchronized void cancelJob()
{
isCancelled = true;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether the thread should cancel what it is
* doing at the earliest opportunity, due to a cancellation request from
* the user.
*
* @return true if the thread should cancel itself at the earliest
* opportunity, otherwise false
*/
protected synchronized boolean isCancelling()
{
if (currentJob.isCancelled())
{
if (!isCancelled)
{
cancelJob();
}
return true;
}
else
{
return false;
}
}
// ----------------------------------------------------------
/**
* Access the current job that this thread is working on.
* @return The current job, or null if there is none
*/
protected Job currentJob()
{
return currentJob;
}
// ----------------------------------------------------------
/**
* Access this worker's local editing context.
* @return The editing context
*/
protected EOEditingContext localContext()
{
if (ec == null)
{
ec = WCEC.newAutoLockingEditingContext();
if (queueDescriptor != null)
{
queueDescriptor = new ManagedQueueDescriptor(
(QueueDescriptor)queueDescriptor.localInstanceIn(ec));
}
if (hostDescriptor != null)
{
hostDescriptor = new ManagedHostDescriptor(
(HostDescriptor)hostDescriptor.localInstanceIn(ec));
}
if (descriptor != null)
{
descriptor = new ManagedWorkerDescriptor(
(WorkerDescriptor)descriptor.localInstanceIn(ec));
}
}
return ec;
}
// ----------------------------------------------------------
/**
* Unlocks the thread's local editing context, recycles it, and then
* relocks it.
*/
protected void renewContext()
{
// Unlock and release the current editing context
ec.dispose();
ec = null;
// Generate a fresh editing context, which will auto-lock on demand
localContext();
}
// ----------------------------------------------------------
/**
* Notify the administrator and any other relevant personnel that the
* current job has been suspended. The job's "isReady" flag is already
* cleared before this is called.
*
*
* @param e the exception thrown by {@link #processJob()}
*/
protected void sendJobSuspensionNotification(Exception e)
{
StringWriter sw = new StringWriter();
if(currentJob.suspensionReason() != null)
{
sw.append(currentJob.suspensionReason());
sw.append("\n\n");
}
sw.append("The worker thread's processJob() method threw the "
+ "following exception:\n\n");
e.printStackTrace(new PrintWriter(sw));
String additionalInfo = additionalSuspensionInfo();
if (additionalInfo != null)
{
sw.append("\n");
sw.append("Additional information about the job:\n");
sw.append(additionalInfo);
}
String reason = sw.toString();
currentJob.setSuspensionReason(reason);
// TODO: replace this with a notification message
log.error("processJob() threw the following exception:", e);
}
// ----------------------------------------------------------
/**
* Gets additional information about the job when it is suspended due to an
* error. This information is included in the suspension reason that is
* shown to the user. Subclasses should override this and provide extra
* information based on fields in the corresponding JobBase subclass.
*
* @return additional information to be shown to the user when the job is
* suspended
*/
protected String additionalSuspensionInfo()
{
return null;
}
// ----------------------------------------------------------
/**
* Waits for a candidate job to become available and tries to take
* ownership of it. This method will not return until it successfully does
* this, at which point the currentJob field will be set to that job.
*/
protected void waitForAvailableJob()
{
boolean didGetJob = false;
if (currentJob != null)
{
return;
}
do
{
// Get a candidate job for this thread to try to take ownership of.
// A candidate will have a null worker relationship, meaning
// nobody else successfully owns it yet.
Job candidate = fetchNextCandidateJob();
if (candidate == null)
{
// If there aren't any jobs currently available, wait
// until something arrives in the queue
logDebug("waiting for queue to wake me");
try
{
queueDescriptor().waitForNextJob();
}
catch (Exception e)
{
// If this blows up, just repeat the loop and try again
}
logDebug("woken by the queue");
}
else
{
// Try to take ownership of the job by setting the worker
// relationship to our worker descriptor and then saving the
// changes. If this succeeds, we own the job. If there is an
// optimistic locking failure, then another thread got it
// first, so we go back to the top of the loop and try to get
// another job.
WorkerDescriptor worker = (WorkerDescriptor)
descriptor().localInstanceIn(localContext());
logDebug("volunteering to run job " + candidate.id());
didGetJob = candidate.volunteerToRun(worker);
if (didGetJob)
{
logDebug("successfully acquired job " + candidate.id());
currentJob = candidate;
}
}
}
while (!didGetJob);
}
// ----------------------------------------------------------
/**
* Fetch the next job that this thread will try to take ownership of.
*
* @return a job that doesn't currently have any worker threads owning it
*/
@SuppressWarnings("unchecked")
protected Job fetchNextCandidateJob()
{
EOEditingContext context = localContext();
String entityName = queueDescriptor().jobEntityName();
EOFetchSpecification fetchSpec = new WCFetchSpecification<Job>(
entityName,
ERXQ.and(
ERXQ.isNull(JobBase.WORKER_KEY),
ERXQ.isFalse(JobBase.IS_CANCELLED_KEY),
ERXQ.isTrue(JobBase.IS_READY_KEY)),
ERXS.sortOrders(JobBase.ENQUEUE_TIME_KEY, ERXS.ASC));
fetchSpec.setFetchLimit(1);
NSArray<Job> jobs = context.objectsWithFetchSpecification(fetchSpec);
if (jobs.count() == 0)
{
return null;
}
else
{
return jobs.objectAtIndex(0);
}
}
//~ Private methods .......................................................
// ----------------------------------------------------------
/**
* Fetches cancelled jobs from the queue and deletes them.
*/
private void killCancelledJobs()
{
Job cancelledJob = fetchNextCancelledJob();
while (cancelledJob != null)
{
cancelledJob.delete();
// If there is an optimistic locking failure when we try to save
// our changes, that's ok because another thread already cancelled
// the job. Continue blissfully on by getting the next job.
try
{
localContext().saveChanges();
}
catch (Exception e)
{
localContext().revert();
}
cancelledJob = fetchNextCancelledJob();
}
}
// ----------------------------------------------------------
/**
* Retrieves cancelled jobs of the type handled by this worker thread.
*
* @return an array of cancelled jobs
*/
@SuppressWarnings("unchecked")
private Job fetchNextCancelledJob()
{
String entityName = queueDescriptor().jobEntityName();
EOFetchSpecification fetchSpec = new EOFetchSpecification(
entityName,
ERXQ.and(
ERXQ.isNull(JobBase.WORKER_KEY),
ERXQ.isTrue(JobBase.IS_CANCELLED_KEY)),
ERXS.sortOrders(JobBase.ENQUEUE_TIME_KEY, ERXS.ASC));
fetchSpec.setFetchLimit(1);
NSArray<Job> jobs =
localContext().objectsWithFetchSpecification(fetchSpec);
if (jobs.count() == 0)
{
return null;
}
else
{
return jobs.objectAtIndex(0);
}
}
// ----------------------------------------------------------
private void logDebug(Object obj)
{
if (log.isDebugEnabled())
{
if (ec != null)
{
ec.lock();
}
log.debug(queueDescriptor().jobEntityName()
+ " worker thread " + getId() + ": " + obj);
if (ec != null)
{
ec.unlock();
}
}
}
//~ Instance/static variables .............................................
private Job currentJob;
private ManagedQueueDescriptor queueDescriptor;
private ManagedHostDescriptor hostDescriptor;
private ManagedWorkerDescriptor descriptor;
private EOEditingContext ec;
private boolean isCancelled;
private static final Logger log = Logger.getLogger(WorkerThread.class);
}