/*
* 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.impl.profile.ProfileCollectorHandle;
import com.sun.sgs.impl.sharedutil.LoggerWrapper;
import com.sun.sgs.impl.util.NamedThreadFactory;
import com.sun.sgs.kernel.KernelRunnable;
import com.sun.sgs.kernel.TaskQueue;
import com.sun.sgs.kernel.RecurringTaskHandle;
import com.sun.sgs.kernel.TaskReservation;
import com.sun.sgs.kernel.TaskScheduler;
import java.util.LinkedList;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Package-private implementation of {@code TaskScheduler} that is used by
* the system scheduling and running all non-transactional, arbitrary-length
* tasks. This is an intentionally simple implementation that uses a backing
* {@code Executor} instead of a {@code SchedulerQueue} until there
* is better understanding of what (if any) custom scheduling behavior will
* help these kinds of tasks.
* <p>
* 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
* non-transactional tasks.<p>
* </dl>
* FIXME: the profiling code needs a way to learn about the thread count
* from this scheduler separately from the transaction pool. When this gets
* added, this class should start tracking thread counts.
*/
final class TaskSchedulerImpl implements TaskScheduler {
// logger for this class
private static final LoggerWrapper logger =
new LoggerWrapper(Logger.getLogger(TaskSchedulerImpl.
class.getName()));
/**
* The property used to define the default number of initial consumer
* threads.
*/
public static final String CONSUMER_THREADS_PROPERTY =
"com.sun.sgs.impl.kernel.task.threads";
/**
* The default number of initial consumer threads.
*/
public static final String DEFAULT_CONSUMER_THREADS = "4";
// the executor used to run tasks
private final ScheduledExecutorService executor;
// the collector handle used for profiling data
private final ProfileCollectorHandle profileCollectorHandle;
// the number of tasks waiting to run
private final AtomicInteger waitingSize = new AtomicInteger(0);
// 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;
/**
* Creates an instance of {@code TaskSchedulerImpl}.
*
* @param properties the {@code Properties} for the system
* @param profileCollectorHandle the {@code ProfileCollectorHandler} used to
* manage collection of per-task profiling data
*
* @throws Exception if there is any failure creating the scheduler
*/
TaskSchedulerImpl(Properties properties,
ProfileCollectorHandle profileCollectorHandle)
throws Exception
{
logger.log(Level.CONFIG, "Creating TaskSchedulerImpl");
if (properties == null) {
throw new NullPointerException("Properties cannot be null");
}
if (profileCollectorHandle == null) {
throw new NullPointerException("Collector handle cannot be null");
}
this.profileCollectorHandle = profileCollectorHandle;
int requestedThreads =
Integer.parseInt(properties.getProperty(CONSUMER_THREADS_PROPERTY,
DEFAULT_CONSUMER_THREADS));
// NOTE: this is replicating previous behavior where there is a
// fixed-size pool for running tasks, but in practice we may
// want a flexible pool that allows (e.g.) for tasks that run
// for the lifetime of a stack
this.executor = Executors.newScheduledThreadPool(
requestedThreads, new NamedThreadFactory("TaskScheduler"));
logger.log(Level.CONFIG,
"Created TaskSchedulerImpl with properties:" +
"\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 TaskScheduler interface.
*/
/**
* {@inheritDoc}
*/
public TaskReservation reserveTask(KernelRunnable task, Identity owner) {
TaskDetail detail = new TaskDetail(task, owner,
System.currentTimeMillis());
return new TaskReservationImpl(detail);
}
/**
* {@inheritDoc}
*/
public TaskReservation reserveTask(KernelRunnable task, Identity owner,
long startTime)
{
return new TaskReservationImpl(new TaskDetail(task, owner, startTime));
}
/**
* {@inheritDoc}
*/
public void scheduleTask(KernelRunnable task, Identity owner) {
try {
TaskDetail detail = new TaskDetail(task, owner,
System.currentTimeMillis());
executor.submit(new TaskRunner(detail));
waitingSize.incrementAndGet();
} catch (RejectedExecutionException ree) {
throw new TaskRejectedException("Couldn't schedule task", ree);
}
}
/**
* {@inheritDoc}
*/
public void scheduleTask(KernelRunnable task, Identity owner,
long startTime)
{
try {
TaskDetail detail = new TaskDetail(task, owner, startTime);
executor.schedule(new TaskRunner(detail),
startTime - System.currentTimeMillis(),
TimeUnit.MILLISECONDS);
waitingSize.incrementAndGet();
} catch (RejectedExecutionException ree) {
throw new TaskRejectedException("Couldn't schedule task", ree);
}
}
/**
* {@inheritDoc}
*/
public RecurringTaskHandle scheduleRecurringTask(KernelRunnable task,
Identity owner,
long startTime,
long period)
{
if (period <= 0) {
throw new IllegalArgumentException("Illegal period: " + period);
}
return new RecurringTaskHandleImpl(new TaskDetail(task, owner,
startTime, period));
}
/**
* {@inheritDoc}
*/
public TaskQueue createTaskQueue() {
if (isShutdown) {
throw new IllegalStateException("Scheduler is shutdown");
}
return new TaskQueueImpl();
}
/*
* Utility methods and classes.
*/
/**
* Tells this scheduler to shutdown.
*
*/
void shutdown() {
synchronized (this) {
if (isShutdown) {
return; // return silently
}
isShutdown = true;
executor.shutdown();
}
}
/** Private implementation of {@code TaskReservation}. */
private class TaskReservationImpl implements TaskReservation {
private final TaskDetail taskDetail;
private boolean usedOrCancelled = false;
/** Creates an instance of {@code TaskReservationImpl}. */
TaskReservationImpl(TaskDetail taskDetail) {
this.taskDetail = taskDetail;
}
/** {@inheritDoc} */
public synchronized void cancel() {
if (usedOrCancelled) {
throw new IllegalStateException("This reservation cannot be " +
"cancelled");
}
usedOrCancelled = true;
}
/**
* {@inheritDoc}
* <p>
* @throws TaskRejectedException if the system has become too
* overloaded to honor this reservation
*/
public void use() {
synchronized (this) {
if (usedOrCancelled) {
throw new IllegalStateException("This reservation cannot " +
"be used");
}
usedOrCancelled = true;
}
try {
long delay = taskDetail.startTime - System.currentTimeMillis();
executor.schedule(new TaskRunner(taskDetail), delay,
TimeUnit.MILLISECONDS);
waitingSize.incrementAndGet();
} catch (RejectedExecutionException ree) {
throw new TaskRejectedException("The system has run out of " +
"resources and cannot run " +
"the requested task", ree);
}
}
}
/** Private implementation of {@code RecurringTaskHandle}. */
private class RecurringTaskHandleImpl implements RecurringTaskHandle {
private final TaskDetail taskDetail;
private boolean isCancelled = false;
private boolean isStarted = false;
private volatile ScheduledFuture<?> future = null;
/** Creates an instance of {@code RecurringTaskHandleImpl}. */
RecurringTaskHandleImpl(TaskDetail taskDetail) {
if (isShutdown) {
throw new IllegalStateException("Scheduler is shutdown");
}
this.taskDetail = taskDetail;
}
/** {@inheritDoc} */
public void cancel() {
synchronized (this) {
if (isCancelled) {
throw new IllegalStateException("Handle already cancelled");
}
isCancelled = true;
}
if (future != null) {
future.cancel(false);
}
}
/** {@inheritDoc} */
public void start() {
synchronized (this) {
if (isCancelled) {
throw new IllegalStateException("Handle already cancelled");
}
if ((future != null) || (isStarted)) {
throw new IllegalStateException("Handle already used");
}
isStarted = true;
}
long delay = taskDetail.startTime - System.currentTimeMillis();
try {
future =
executor.scheduleAtFixedRate(new TaskRunner(taskDetail),
delay, taskDetail.period,
TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException ree) {
throw new TaskRejectedException("The system has run out of " +
"resources and cannot start " +
"the requested task", ree);
}
}
}
/** Private class used to maintain task detail. */
private static class TaskDetail {
final KernelRunnable task;
final Identity owner;
volatile long startTime;
final long period;
final TaskQueueImpl queue;
/** Creates an instance of {@code TaskDetail}. */
TaskDetail(KernelRunnable task, Identity owner, long startTime) {
this(task, owner, startTime, 0);
}
/** Creates an instance of {@code TaskDetail}. */
TaskDetail(KernelRunnable task, Identity owner, long startTime,
long period)
{
if (task == null) {
throw new NullPointerException("Task cannot be null");
}
if (owner == null) {
throw new NullPointerException("Owner cannot be null");
}
this.task = task;
this.owner = owner;
this.startTime = startTime;
this.period = period;
this.queue = null;
}
/** Creates an instance of {@code TaskDetail} with dependency. */
TaskDetail(KernelRunnable task, Identity owner, TaskQueueImpl queue) {
if (task == null) {
throw new NullPointerException("Task cannot be null");
}
if (owner == null) {
throw new NullPointerException("Owner cannot be null");
}
if (queue == null) {
throw new NullPointerException("TaskQueue cannot be null");
}
this.task = task;
this.owner = owner;
this.startTime = System.currentTimeMillis();
this.period = 0;
this.queue = queue;
}
/** Returns whether this task is recurring. */
boolean isRecurring() {
return period != 0;
}
}
/**
* Private {@code Runnable} used to wrap all {@code KernelRunnable} tasks
* submitted to this scheduler.
*/
private class TaskRunner implements Runnable {
private final TaskDetail taskDetail;
/** Creates an instance of {@code TaskRunner} to run the task. */
TaskRunner(TaskDetail taskDetail) {
this.taskDetail = taskDetail;
}
/** {@inheritDoc} */
public void run() {
logger.log(Level.FINE, "Running a non-transactional task");
int queueSize = (taskDetail.isRecurring() ? waitingSize.get() :
waitingSize.decrementAndGet());
profileCollectorHandle.startTask(taskDetail.task, taskDetail.owner,
taskDetail.startTime, queueSize);
if (taskDetail.isRecurring()) {
taskDetail.startTime += taskDetail.period;
}
// store the current owner, and then push the new thread detail
Identity parent = ContextResolver.getCurrentOwner();
ContextResolver.setTaskState(kernelContext, taskDetail.owner);
try {
taskDetail.task.run();
profileCollectorHandle.finishTask(1);
} catch (Exception e) {
profileCollectorHandle.finishTask(1, e);
if (logger.isLoggable(Level.WARNING)) {
if (taskDetail.isRecurring()) {
logger.logThrow(Level.WARNING, e, "failed to run " +
"task {0}", taskDetail.task);
} else {
logger.logThrow(Level.WARNING, e, "failed to run " +
"recurrence of task {0}",
taskDetail.task);
}
}
} finally {
// always restore the previous owner before leaving
ContextResolver.setTaskState(kernelContext, parent);
// schedule the next task, if any
if (taskDetail.queue != null) {
taskDetail.queue.scheduleNextTask();
}
}
}
}
/** Private implementation of {@code TaskQueue}. */
private final class TaskQueueImpl implements TaskQueue {
private final LinkedList<TaskDetail> queue =
new LinkedList<TaskDetail>();
private boolean inScheduler = false;
/** {@inheritDoc} */
public void addTask(KernelRunnable task, Identity owner) {
TaskDetail detail = new TaskDetail(task, owner, this);
waitingSize.incrementAndGet();
synchronized (this) {
if (inScheduler) {
queue.offer(detail);
} else {
inScheduler = true;
executor.submit(new TaskRunner(detail));
}
}
}
/** Private method to schedule the next task, if any. */
void scheduleNextTask() {
synchronized (this) {
if (queue.isEmpty()) {
inScheduler = false;
} else {
// re-set the start time before scheduling, since the
// task isn't really requested to start until all
// tasks ahead of it have run
TaskDetail detail = queue.poll();
detail.startTime = System.currentTimeMillis();
executor.submit(new TaskRunner(detail));
}
}
}
}
}