/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server 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 General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.kernel;
import com.sun.sgs.app.TaskRejectedException;
import com.sun.sgs.auth.Identity;
import com.sun.sgs.kernel.schedule.ScheduledTask;
import com.sun.sgs.kernel.schedule.SchedulerQueue;
import com.sun.sgs.kernel.schedule.SchedulerRetryPolicy;
import com.sun.sgs.impl.profile.ProfileCollectorHandle;
import com.sun.sgs.impl.service.transaction.TransactionCoordinator;
import com.sun.sgs.impl.service.transaction.TransactionHandle;
import com.sun.sgs.impl.sharedutil.LoggerWrapper;
import com.sun.sgs.impl.sharedutil.PropertiesWrapper;
import com.sun.sgs.impl.util.NamedThreadFactory;
import com.sun.sgs.kernel.KernelRunnable;
import com.sun.sgs.kernel.Priority;
import com.sun.sgs.kernel.PriorityScheduler;
import com.sun.sgs.kernel.RecurringTaskHandle;
import com.sun.sgs.kernel.TaskQueue;
import com.sun.sgs.kernel.TaskReservation;
import com.sun.sgs.kernel.TransactionScheduler;
import com.sun.sgs.profile.ProfileListener;
import com.sun.sgs.profile.ProfileReport;
import com.sun.sgs.service.Transaction;
import java.beans.PropertyChangeEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Package-private implementation of {@code TransactionScheduler} that is
* used by the system for scheduling and running all transactional tasks.
* This class supports the following configuration properties:
* <dl style="margin-left: 1em">
*
* <dt> <i>Property:</i> <code><b>{@value #CONSUMER_THREADS_PROPERTY}
* </b></code> <br>
* <i>Default:</i> <code>{@value #DEFAULT_CONSUMER_THREADS}</code>
*
* <dd style="padding-top: .5em">The number of initial threads used to process
* transactional tasks.<p>
*
* <dt> <i>Property:</i> <code><b>{@value #SCHEDULER_QUEUE_PROPERTY}
* </b></code> <br>
* <i>Default:</i> <code>{@value #DEFAULT_SCHEDULER_QUEUE}</code>
*
* <dd style="padding-top: .5em">The implementation class used to track
* access to define which queue implementation should back this scheduler.
* The value of this property should be the
* name of a public, non-abstract class that implements the
* {@link SchedulerQueue} interface, and that provides a public
* constructor with the parameters {@link Properties}<p>
*
* <dt> <i>Property:</i> <code><b>{@value #SCHEDULER_RETRY_PROPERTY}
* </b></code> <br>
* <i>Default:</i> <code>{@value #DEFAULT_SCHEDULER_RETRY}</code>
*
* <dd style="padding-top: .5em">The implementation class used to define
* which retry policy implementation to use when tasks fail or abort.
* The value of this property should be the
* name of a public, non-abstract class that implements the
* {@link SchedulerRetryPolicy} interface, and that provides a public
* constructor with the parameters {@link Properties}<p>
*
* </dl>
*/
final class TransactionSchedulerImpl
implements TransactionScheduler, PriorityScheduler, ProfileListener
{
// logger for this class
private static final LoggerWrapper logger =
new LoggerWrapper(Logger.getLogger(TransactionSchedulerImpl.
class.getName()));
/**
* The property used to define which queue implementation should back
* this scheduler.
*/
public static final String SCHEDULER_QUEUE_PROPERTY =
"com.sun.sgs.impl.kernel.scheduler.queue";
/**
* The default scheduler.
*/
public static final String DEFAULT_SCHEDULER_QUEUE =
"com.sun.sgs.impl.kernel.schedule.FIFOSchedulerQueue";
/**
* The property used to define which retry policy should be used in
* this scheduler
*/
public static final String SCHEDULER_RETRY_PROPERTY =
"com.sun.sgs.impl.kernel.scheduler.retry";
/**
* The default retry policy
*/
public static final String DEFAULT_SCHEDULER_RETRY =
"com.sun.sgs.impl.kernel.schedule.ImmediateRetryPolicy";
/**
* The property used to define the default number of initial consumer
* threads.
*/
public static final String CONSUMER_THREADS_PROPERTY =
"com.sun.sgs.impl.kernel.transaction.threads";
/**
* The default number of initial consumer threads.
*/
public static final String DEFAULT_CONSUMER_THREADS = "4";
// the default priority for tasks
private static final Priority defaultPriority =
Priority.getDefaultPriority();
// the coordinator used to create and coordinate transactions
private final TransactionCoordinator transactionCoordinator;
// the backing scheduler queue used for ordering tasks
private final SchedulerQueue backingQueue;
// the retry policy used for this scheduler
private final SchedulerRetryPolicy retryPolicy;
// the collector handle used for profiling data
private final ProfileCollectorHandle profileCollectorHandle;
// the coordinator for all transactional object access
private final AccessCoordinatorHandle accessCoordinator;
// the executor service used to manage our threads
private final ExecutorService executor;
// the actual number of threads we're currently using
private final AtomicInteger threadCount = new AtomicInteger(0);
// the number of requested consumer threads
private final int requestedThreads;
// flag to note that this scheduler has shutdown
private volatile boolean isShutdown = false;
// the context we're using for the application's tasks
private volatile KernelContext kernelContext = null;
// the number of dependent tasks sitting in queues
private final AtomicInteger dependencyCount = new AtomicInteger(0);
/**
* Creates an instance of {@code TransactionSchedulerImpl}.
*
* @param properties the {@code Properties} for the system
* @param transactionCoordinator the {@code TransactionCoordinator} used
* by the system to manage transactions
* @param profileCollectorHandle the {@code ProfileCollectorHandler} used to
* manage collection of per-task profiling data
* @param accessCoordinator the {@code AccessCoordinator} used by
* the system to managed shared data
*
* @throws InvocationTargetException if there is a failure initializing
* the {@code SchedulerQueue}
* @throws Exception if there is any failure creating the scheduler
*/
TransactionSchedulerImpl(Properties properties,
TransactionCoordinator transactionCoordinator,
ProfileCollectorHandle profileCollectorHandle,
AccessCoordinatorHandle accessCoordinator)
throws Exception
{
logger.log(Level.CONFIG, "Creating TransactionSchedulerImpl");
if (properties == null) {
throw new NullPointerException("Properties cannot be null");
}
if (transactionCoordinator == null) {
throw new NullPointerException("Coordinator cannot be null");
}
if (profileCollectorHandle == null) {
throw new NullPointerException("Collector handle cannot be null");
}
if (accessCoordinator == null) {
throw new NullPointerException("AccessCoordinator cannot be null");
}
PropertiesWrapper wrappedProps = new PropertiesWrapper(properties);
this.transactionCoordinator = transactionCoordinator;
this.profileCollectorHandle = profileCollectorHandle;
this.accessCoordinator = accessCoordinator;
this.backingQueue = wrappedProps.getClassInstanceProperty(
SCHEDULER_QUEUE_PROPERTY, DEFAULT_SCHEDULER_QUEUE,
SchedulerQueue.class, new Class[]{Properties.class},
properties);
this.retryPolicy = wrappedProps.getClassInstanceProperty(
SCHEDULER_RETRY_PROPERTY, DEFAULT_SCHEDULER_RETRY,
SchedulerRetryPolicy.class, new Class[]{Properties.class},
properties);
// startup the requested number of consumer threads
// NOTE: this is a simple implmentation to replicate the previous
// behvavior, with the assumption that it will change if the
// scheduler starts trying to add or drop consumers adaptively
this.requestedThreads =
Integer.parseInt(properties.getProperty(CONSUMER_THREADS_PROPERTY,
DEFAULT_CONSUMER_THREADS));
this.executor = Executors.newCachedThreadPool(
new NamedThreadFactory("TransactionScheduler"));
for (int i = 0; i < requestedThreads; i++) {
executor.submit(new TaskConsumer());
}
// initialize the default timeout for scheduled tasks
ScheduledTaskImpl.Builder.setDefaultTimeout(
transactionCoordinator.getDefaultTimeout());
logger.log(Level.CONFIG,
"Created TransactionSchedulerImpl with properties:" +
"\n " + SCHEDULER_RETRY_PROPERTY + "=" +
retryPolicy.getClass().getName() +
"\n " + SCHEDULER_QUEUE_PROPERTY + "=" +
backingQueue.getClass().getName() +
"\n " + CONSUMER_THREADS_PROPERTY + "=" + requestedThreads);
}
/**
* Package-private method used to set the context being used by the kernel.
*
* @param kernelContext the {@code KernelContext} for this scheduler
*/
void setContext(KernelContext kernelContext) {
this.kernelContext = kernelContext;
}
/*
* Implementations of the TransactionScheduler interface.
*/
/**
* {@inheritDoc}
*/
public TaskReservation reserveTask(KernelRunnable task, Identity owner) {
ScheduledTaskImpl t = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).build();
return backingQueue.reserveTask(t);
}
/**
* {@inheritDoc}
*/
public TaskReservation reserveTask(KernelRunnable task, Identity owner,
long startTime)
{
ScheduledTaskImpl t = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).startTime(startTime).build();
return backingQueue.reserveTask(t);
}
/**
* {@inheritDoc}
*/
public void scheduleTask(KernelRunnable task, Identity owner) {
backingQueue.addTask(new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).build());
}
/**
* {@inheritDoc}
*/
public void scheduleTask(KernelRunnable task, Identity owner,
long startTime)
{
backingQueue.addTask(new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).startTime(startTime).build());
}
/**
* {@inheritDoc}
*/
public RecurringTaskHandle scheduleRecurringTask(KernelRunnable task,
Identity owner,
long startTime,
long period)
{
ScheduledTaskImpl scheduledTask = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).
startTime(startTime).
period(period).
build();
RecurringTaskHandle handle =
backingQueue.createRecurringTaskHandle(scheduledTask);
scheduledTask.setRecurringTaskHandle(handle);
return handle;
}
/**
* {@inheritDoc}
*/
public TaskQueue createTaskQueue() {
if (isShutdown) {
throw new IllegalStateException("Scheduler is shutdown");
}
return new TaskQueueImpl();
}
/**
* {@inheritDoc}
*/
public void runTask(KernelRunnable task, Identity owner) throws Exception {
if (isShutdown) {
throw new IllegalStateException("Scheduler is shutdown");
}
if (ContextResolver.isCurrentTransaction()) {
// we're already active in a transaction, so just run the task
task.run();
} else {
// we're starting a new transaction
ScheduledTaskImpl scheduledTask = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).build();
waitForTask(scheduledTask);
}
}
/*
* Implementations of the PriorityScheduler interface.
*/
/**
* {@inheritDoc}
*/
public TaskReservation reserveTask(KernelRunnable task, Identity owner,
Priority priority)
{
ScheduledTaskImpl t = new ScheduledTaskImpl.Builder(
task, owner, priority).build();
return backingQueue.reserveTask(t);
}
/**
* {@inheritDoc}
*/
public void scheduleTask(KernelRunnable task, Identity owner,
Priority priority)
{
backingQueue.addTask(new ScheduledTaskImpl.Builder(
task, owner, priority).build());
}
/*
* Implementations for the ProfileListener interface.
*/
/**
* {@inheritDoc}
*/
public void propertyChange(PropertyChangeEvent event) {
// see comment in notifyThreadLeaving
}
/**
* {@inheritDoc}
*/
public void report(ProfileReport profileReport) {
// see comment in notifyThreadLeaving
}
/**
* {@inheritDoc}
*/
public void shutdown() {
synchronized (this) {
if (isShutdown) {
return; // return silently
}
isShutdown = true;
executor.shutdownNow();
backingQueue.shutdown();
}
}
/*
* Utility methods and classes.
*/
/**
* Private method that blocks until the task has completed, re-throwing
* any exception resulting from the task failing.
*/
private void waitForTask(ScheduledTaskImpl task)
throws Exception
{
Throwable t = null;
try {
// NOTE: calling executeTask() directly means that we're trying
// to run the transaction in the calling thread, so there are
// actually more threads running tasks simulaneously than there
// are threads in the scheduler pool. This could be changed to
// hand-off the task and wait for the result if we wanted more
// direct control over concurrent transactions
executeTask(task, false);
// wait for the task to complete...at this point it may have
// already completed, or else it is being re-tried in a
// scheduler thread
t = task.get();
} catch (InterruptedException ie) {
// we were interrupted, so try to cancel the task, re-throwing
// the interruption if that succeeds or looking at the result
// if the task completes before it can be cancelled
if (task.cancel(false)) {
backingQueue.notifyCancelled(task);
throw ie;
}
if (task.isCancelled()) {
throw ie;
}
t = task.get();
}
// if the result of the task was a permananent failure, then
// re-throw the exception
if (t != null) {
if (t instanceof Exception) {
throw (Exception) t;
}
throw (Error) t;
}
}
/**
* Package-private method that runs the given task in a transaction that
* is not bound by any timeout value (i.e., is bound only by the
* {@code com.sun.sgs.txn.timeout.unbounded} property value).
*
* @param task the {@code KernelRunnable} to run transactionally
* @param owner the {@code Identity} that owns the task
*
* @throws IllegalStateException if this method is called from an
* actively running transaction
* @throws Exception if there is any failure that does not result in
* re-trying the task
*/
void runUnboundedTask(KernelRunnable task, Identity owner)
throws Exception
{
if (isShutdown) {
throw new IllegalStateException("Scheduler is shutdown");
}
if (ContextResolver.isCurrentTransaction()) {
throw new IllegalStateException("Cannot be called from within " +
"an active transaction");
}
// NOTE: in the current system we only use this method once, and
// that's when the application is initialized, in which case there
// is no other task trying to run...if we decide to start using
// this method more broadly, then it should probably use a separate
// thread-pool so that it doesn't affect transaction latency
ScheduledTaskImpl scheduledTask = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).
timeout(ScheduledTask.UNBOUNDED).
build();
waitForTask(scheduledTask);
}
/**
* Notifies the scheduler that a thread has been started to consume
* tasks as they become ready.
*/
private void notifyThreadJoining() {
profileCollectorHandle.notifyThreadAdded();
threadCount.incrementAndGet();
}
/**
* Notifies the scheduler that a thread has been interrupted and is
* finishing its work.
*/
private void notifyThreadLeaving() {
profileCollectorHandle.notifyThreadRemoved();
// NOTE: we're not yet trying to adapt the number of threads being
// used, so we assume that threads are only lost when the system
// wants to shutdown...in practice, this should look at some
// threshold and see if another consumer needs to be created
if (threadCount.decrementAndGet() == 0) {
logger.log(Level.CONFIG, "No more threads are consuming tasks");
shutdown();
}
}
/**
* Private {@code Runnable} used to consume tasks as they become available
* from the {@code SchedulerQueue}. Once started, it will continue
* running until it catches an {@code InterruptedException}.
*/
private class TaskConsumer implements Runnable {
/** {@inheritDoc} */
public void run() {
logger.log(Level.FINE, "Starting a consumer for transactions");
notifyThreadJoining();
try {
while (true) {
// wait for the next task, at which point we may get
// interrupted and should therefore return
ScheduledTaskImpl task =
(ScheduledTaskImpl) (backingQueue.getNextTask(true));
// run the task, checking if it completed
if (executeTask(task, true)) {
// if it's a recurring task, schedule the next run
if (task.isRecurring()) {
long nextStart =
task.getStartTime() + task.getPeriod();
task = new ScheduledTaskImpl.Builder(
task).startTime(nextStart).build();
backingQueue.addTask(task);
}
// if it has dependent tasks, schedule the next one
TaskQueueImpl queue =
(TaskQueueImpl) (task.getTaskQueue());
if (queue != null) {
queue.scheduleNextTask();
}
}
}
} catch (InterruptedException ie) {
if (logger.isLoggable(Level.FINE)) {
logger.logThrow(Level.FINE, ie, "Consumer is finishing");
}
} catch (Exception e) {
// this should never happen, since running the task should
// never throw an exception that isn't handled
logger.logThrow(Level.SEVERE, e, "Fatal error for consumer");
} finally {
notifyThreadLeaving();
}
}
}
/**
* Private method that executes a single task, creating the transaction
* state and handling re-try as appropriate. If the thread calling this
* method is interrupted before the task can complete then this method
* attempts to re-schedule the task to run in another thread if
* {@code retryOnInterruption} is {@code true} and always re-throws
* the associated {@code InterruptedException}.
* <p>
* This method returns {@code true} if the task was completed or failed
* permanently, and {@code false} otherwise. If {@code false} is returned
* then the task is scheduled to be re-tried at some point in the future,
* possibly by another thread, by this method. The caller may query the
* status of the task and wait for the task to complete or fail permanently
* through the {@code ScheduledTaskImpl} interface.
*/
private boolean executeTask(ScheduledTaskImpl task,
boolean retryOnInterruption)
throws InterruptedException
{
logger.log(Level.FINEST, "starting a new transactional task");
// store the current owner, and then push the new thread detail
Identity parent = ContextResolver.getCurrentOwner();
ContextResolver.setTaskState(kernelContext, task.getOwner());
try {
// keep trying to run the task until we succeed, tracking how
// many tries it actually took
while (true) {
if (!task.setRunning(true)) {
// this task is already finished
return true;
}
// NOTE: We could report the queue sizes separately,
// so we should figure out how we want to represent these
int waitSize =
backingQueue.getReadyCount() +
dependencyCount.get();
profileCollectorHandle.startTask(task.getTask(),
task.getOwner(),
task.getStartTime(),
waitSize);
task.incrementTryCount();
Transaction transaction = null;
try {
// setup the transaction state
TransactionHandle handle =
transactionCoordinator.createTransaction(
task.getTimeout());
transaction = handle.getTransaction();
ContextResolver.setCurrentTransaction(transaction);
try {
// notify the profiler and access coordinator
profileCollectorHandle.noteTransactional(
transaction.getId());
accessCoordinator.
notifyNewTransaction(transaction,
task.getStartTime(),
task.getTryCount());
// run the task in the new transactional context
task.getTask().run();
} finally {
// regardless of the outcome, always clear the current
// transaction state before proceeding...
ContextResolver.clearCurrentTransaction(transaction);
}
// try to commit the transaction...note that there's the
// chance that the application code masked the orginal
// cause of a failure, so we'll check for that first,
// re-throwing the root cause in that case
if (transaction.isAborted()) {
throw transaction.getAbortCause();
}
handle.commit();
// the task completed successfully, so we're done
profileCollectorHandle.finishTask(task.getTryCount());
task.setDone(null);
return true;
} catch (InterruptedException ie) {
// make sure the transaction was aborted
if (!transaction.isAborted()) {
transaction.abort(ie);
}
profileCollectorHandle.finishTask(task.getTryCount(), ie);
task.setLastFailure(ie);
// if the task didn't finish because of the interruption
// then we want to note that and possibly re-queue the
// task to run in a usable thread
if (task.setInterrupted() && retryOnInterruption) {
if (!handoff(task)) {
// if the task couldn't be re-queued, then there's
// nothing left to do but drop it
task.setDone(ie);
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(Level.WARNING, ie,
"dropping an " +
"interrupted task: {0}",
task);
}
}
}
// always re-throw the interruption
throw ie;
} catch (Throwable t) {
// make sure the transaction was aborted
if ((transaction != null) && (!transaction.isAborted())) {
transaction.abort(t);
}
profileCollectorHandle.finishTask(task.getTryCount(), t);
task.setLastFailure(t);
// some error occurred, so see if we should re-try
switch (retryPolicy.getRetryAction(task)) {
case DROP:
task.setDone(t);
if (logger.isLoggable(Level.WARNING)) {
if (task.isRecurring()) {
logger.logThrow(Level.WARNING, t,
"skipping a recurrence " +
"of a task that failed: " +
"{0}", task);
} else {
logger.logThrow(Level.WARNING, t,
"dropping a task that " +
"failed: {0}", task);
}
}
return true;
case RETRY_LATER:
task.setRunning(false);
if (handoff(task)) {
return false;
}
break;
case RETRY_NOW:
task.setRunning(false);
break;
default:
// we should never get here
}
}
}
} finally {
// always restore the previous owner before leaving...
ContextResolver.setTaskState(kernelContext, parent);
}
}
/**
* Hands off the task to the backing queue.
*
* @param task the task to handoff
* @return {@code true} if handoff was successful, {@code false} otherwise
*/
private boolean handoff(ScheduledTaskImpl task) {
try {
backingQueue.addTask(task);
return true;
} catch (TaskRejectedException tre) {
return false;
}
}
/** Private implementation of {@code TaskQueue}. */
private final class TaskQueueImpl implements TaskQueue {
private final Queue<ScheduledTaskImpl> queue =
new LinkedList<ScheduledTaskImpl>();
private boolean inScheduler = false;
/** {@inheritDoc} */
public void addTask(KernelRunnable task, Identity owner) {
ScheduledTaskImpl schedTask = new ScheduledTaskImpl.Builder(
task, owner, defaultPriority).build();
schedTask.setTaskQueue(this);
synchronized (this) {
if (inScheduler) {
dependencyCount.incrementAndGet();
queue.offer(schedTask);
} else {
inScheduler = true;
backingQueue.addTask(schedTask);
}
}
}
/** Private method to schedule the next task, if any. */
void scheduleNextTask() {
synchronized (this) {
if (queue.isEmpty()) {
inScheduler = false;
} else {
dependencyCount.decrementAndGet();
// re-set the start time before scheduling, since the
// task isn't really requested to start until all
// tasks ahead of it have run
ScheduledTaskImpl schedTask = queue.poll();
schedTask.resetStartTime();
backingQueue.addTask(schedTask);
}
}
}
}
}