package org.threadly.concurrent;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.LockSupport;
import org.threadly.util.AbstractService;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
/**
* Executor to run tasks, schedule tasks. Unlike
* {@link java.util.concurrent.ScheduledThreadPoolExecutor} this scheduled executor's pool size
* can shrink if set with a lower value via {@link #setPoolSize(int)}. It also has the benefit
* that you can provide "low priority" tasks.
* <p>
* These low priority tasks will delay their execution if there are other high priority tasks
* ready to run, as long as they have not exceeded their maximum wait time. If they have exceeded
* their maximum wait time, and high priority tasks delay time is less than the low priority delay
* time, then those low priority tasks will be executed. What this results in is a task which has
* lower priority, but which wont be starved from execution.
* <p>
* Most tasks provided into this pool will likely want to be "high priority", to more closely
* match the behavior of other thread pools. That is why unless specified by the constructor, the
* default {@link TaskPriority} is High.
* <p>
* In all conditions, "low priority" tasks will never be starved. This makes "low priority" tasks
* ideal which do regular cleanup, or in general anything that must run, but cares little if there
* is a 1, or 10 second gap in the execution time. That amount of tolerance is adjustable by
* setting the {@code maxWaitForLowPriorityInMs} either in the constructor, or at runtime via
* {@link #setMaxWaitForLowPriority(long)}.
*
* @since 2.2.0 (since 1.0.0 as PriorityScheduledExecutor)
*/
public class PriorityScheduler extends AbstractPriorityScheduler {
protected static final boolean DEFAULT_NEW_THREADS_DAEMON = true;
protected final WorkerPool workerPool;
protected final QueueManager taskQueueManager;
/**
* Constructs a new thread pool, though threads will be lazily started as it has tasks ready to
* run. This constructs a default priority of high (which makes sense for most use cases). It
* also defaults low priority task wait as 500ms. It also defaults to all newly created threads
* to being daemon threads.
*
* @param poolSize Thread pool size that should be maintained
*/
public PriorityScheduler(int poolSize) {
this(poolSize, null, DEFAULT_LOW_PRIORITY_MAX_WAIT_IN_MS, DEFAULT_NEW_THREADS_DAEMON);
}
/**
* Constructs a new thread pool, though threads will be lazily started as it has tasks ready to
* run. This constructs a default priority of high (which makes sense for most use cases). It
* also defaults low priority task wait as 500ms.
*
* @param poolSize Thread pool size that should be maintained
* @param useDaemonThreads {@code true} if newly created threads should be daemon
*/
public PriorityScheduler(int poolSize, boolean useDaemonThreads) {
this(poolSize, null, DEFAULT_LOW_PRIORITY_MAX_WAIT_IN_MS, useDaemonThreads);
}
/**
* Constructs a new thread pool, though threads will be lazily started as it has tasks ready to
* run. This provides the extra parameters to tune what tasks submitted without a priority
* will be scheduled as. As well as the maximum wait for low priority tasks.
*
* @param poolSize Thread pool size that should be maintained
* @param defaultPriority Default priority for tasks which are submitted without any specified priority
* @param maxWaitForLowPriorityInMs time low priority tasks to wait if there are high priority tasks ready to run
*/
public PriorityScheduler(int poolSize, TaskPriority defaultPriority,
long maxWaitForLowPriorityInMs) {
this(poolSize, defaultPriority, maxWaitForLowPriorityInMs, DEFAULT_NEW_THREADS_DAEMON);
}
/**
* Constructs a new thread pool, though threads will be lazily started as it has tasks ready to
* run. This provides the extra parameters to tune what tasks submitted without a priority
* will be scheduled as. As well as the maximum wait for low priority tasks.
*
* @param poolSize Thread pool size that should be maintained
* @param defaultPriority Default priority for tasks which are submitted without any specified priority
* @param maxWaitForLowPriorityInMs time low priority tasks to wait if there are high priority tasks ready to run
* @param useDaemonThreads {@code true} if newly created threads should be daemon
*/
public PriorityScheduler(int poolSize, TaskPriority defaultPriority,
long maxWaitForLowPriorityInMs, boolean useDaemonThreads) {
this(poolSize, defaultPriority, maxWaitForLowPriorityInMs,
new ConfigurableThreadFactory(PriorityScheduler.class.getSimpleName() + "-",
true, useDaemonThreads, Thread.NORM_PRIORITY, null, null));
}
/**
* Constructs a new thread pool, though threads will be lazily started as it has tasks ready to
* run. This provides the extra parameters to tune what tasks submitted without a priority
* will be scheduled as. As well as the maximum wait for low priority tasks.
*
* @param poolSize Thread pool size that should be maintained
* @param defaultPriority Default priority for tasks which are submitted without any specified priority
* @param maxWaitForLowPriorityInMs time low priority tasks to wait if there are high priority tasks ready to run
* @param threadFactory thread factory for producing new threads within executor
*/
public PriorityScheduler(int poolSize, TaskPriority defaultPriority,
long maxWaitForLowPriorityInMs, ThreadFactory threadFactory) {
this(new WorkerPool(threadFactory, poolSize),
defaultPriority, maxWaitForLowPriorityInMs);
}
/**
* This constructor is designed for extending classes to be able to provide their own
* implementation of {@link WorkerPool}. Ultimately all constructors will defer to this one.
*
* @param workerPool WorkerPool to handle accepting tasks and providing them to a worker for execution
* @param defaultPriority Default priority to store in case no priority is provided for tasks
* @param maxWaitForLowPriorityInMs time low priority tasks to wait if there are high priority tasks ready to run
*/
protected PriorityScheduler(WorkerPool workerPool, TaskPriority defaultPriority,
long maxWaitForLowPriorityInMs) {
super(defaultPriority);
this.workerPool = workerPool;
taskQueueManager = new QueueManager(workerPool, maxWaitForLowPriorityInMs);
workerPool.start(taskQueueManager);
}
/**
* Getter for the currently set max thread pool size.
*
* @return current max pool size
*/
public int getMaxPoolSize() {
return workerPool.getMaxPoolSize();
}
/**
* Getter for the current quantity of threads running in this pool (either active or idle).
* This is different than the size returned from {@link #getMaxPoolSize()} in that we
* lazily create threads. This represents the amount of threads needed to be created so far,
* where {@link #getMaxPoolSize()} represents the amount of threads the pool may grow to.
*
* @return current thread count
*/
public int getCurrentPoolSize() {
return workerPool.getCurrentPoolSize();
}
/**
* Change the set thread pool size.
* <p>
* If the value is less than the current running threads, as threads finish they will exit
* rather than accept new tasks. No currently running tasks will be interrupted, rather we
* will just wait for them to finish before killing the thread.
* <p>
* If this is an increase in the pool size, threads will be lazily started as needed till the
* new size is reached. If there are tasks waiting for threads to run on, they immediately
* will be started.
*
* @param newPoolSize New core pool size, must be at least one
*/
public void setPoolSize(int newPoolSize) {
workerPool.setPoolSize(newPoolSize);
}
/**
* Call to check how many tasks are currently being executed in this thread pool. Unlike
* {@link #getCurrentPoolSize()}, this count will NOT include idle threads waiting to execute
* tasks.
*
* @return current number of running tasks
*/
@Override
public int getActiveTaskCount() {
return workerPool.getActiveTaskCount();
}
/**
* Ensures all threads have been started, it will create threads till the thread count matches
* the set pool size (checked via {@link #getMaxPoolSize()}). These new threads will remain
* idle till there is tasks ready to execute.
*/
public void prestartAllThreads() {
workerPool.prestartAllThreads();
}
@Override
public boolean isShutdown() {
return workerPool.isShutdownStarted();
}
/**
* Stops any new tasks from being submitted to the pool. But allows all tasks which are
* submitted to execute, or scheduled (and have elapsed their delay time) to run. If recurring
* tasks are present they will also be unable to reschedule. If {@code shutdown()} or
* {@link #shutdownNow()} has already been called, this will have no effect.
* <p>
* If you wish to not want to run any queued tasks you should use {@link #shutdownNow()}.
*/
public void shutdown() {
if (workerPool.startShutdown()) {
ShutdownRunnable sr = new ShutdownRunnable(workerPool);
taskQueueManager.highPriorityQueueSet
.addExecute(new OneTimeTaskWrapper(sr, taskQueueManager.highPriorityQueueSet.executeQueue,
Clock.lastKnownForwardProgressingMillis()));
}
}
/**
* Stops any new tasks from being able to be executed and removes workers from the pool.
* <p>
* This implementation refuses new submissions after this call. And will NOT interrupt any
* tasks which are currently running. However any tasks which are waiting in queue to be run
* (but have not started yet), will not be run. Those waiting tasks will be removed, and as
* workers finish with their current tasks the threads will be joined.
*
* @return List of runnables which were waiting to execute
*/
public List<Runnable> shutdownNow() {
workerPool.startShutdown();
List<Runnable> awaitingTasks = taskQueueManager.clearQueue();
workerPool.finishShutdown();
return awaitingTasks;
}
/**
* Block until the thread pool has shutdown and all threads have been stopped. If neither
* {@link #shutdown()} or {@link #shutdownNow()} is invoked, then this will block forever.
*
* @throws InterruptedException Thrown if blocking thread is interrupted waiting for shutdown
*/
public void awaitTermination() throws InterruptedException {
awaitTermination(Long.MAX_VALUE);
}
/**
* Block until the thread pool has shutdown and all threads have been stopped. If neither
* {@link #shutdown()} or {@link #shutdownNow()} is invoked, then this will block until the
* timeout is reached.
*
* @param timeoutMillis time to block and wait for thread pool to shutdown
* @return {@code true} if the pool has shutdown, false if timeout was reached
* @throws InterruptedException Thrown if blocking thread is interrupted waiting for shutdown
*/
public boolean awaitTermination(long timeoutMillis) throws InterruptedException {
return workerPool.awaitTermination(timeoutMillis);
}
@Override
public int getQueuedTaskCount() {
// subtract one for hack task for spin issue
return super.getQueuedTaskCount() - 1;
}
@Override
public int getQueuedTaskCount(TaskPriority priority) {
// subtract one from starvable count for hack task for spin issue
return super.getQueuedTaskCount(priority) - (priority == TaskPriority.Starvable ? 1 : 0);
}
@Override
protected OneTimeTaskWrapper doSchedule(Runnable task, long delayInMillis, TaskPriority priority) {
QueueSet queueSet = taskQueueManager.getQueueSet(priority);
OneTimeTaskWrapper result;
if (delayInMillis == 0) {
addToExecuteQueue(queueSet,
(result = new OneTimeTaskWrapper(task, queueSet.executeQueue,
Clock.lastKnownForwardProgressingMillis())));
} else {
addToScheduleQueue(queueSet,
(result = new OneTimeTaskWrapper(task, queueSet.scheduleQueue,
Clock.accurateForwardProgressingMillis() +
delayInMillis)));
}
return result;
}
@Override
public void scheduleWithFixedDelay(Runnable task, long initialDelay,
long recurringDelay, TaskPriority priority) {
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay");
ArgumentVerifier.assertNotNegative(recurringDelay, "recurringDelay");
if (priority == null) {
priority = defaultPriority;
}
QueueSet queueSet = taskQueueManager.getQueueSet(priority);
addToScheduleQueue(queueSet,
new RecurringDelayTaskWrapper(task, queueSet,
Clock.accurateForwardProgressingMillis() +
initialDelay,
recurringDelay));
}
@Override
public void scheduleAtFixedRate(Runnable task, long initialDelay, long period,
TaskPriority priority) {
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay");
ArgumentVerifier.assertGreaterThanZero(period, "period");
if (priority == null) {
priority = defaultPriority;
}
QueueSet queueSet = taskQueueManager.getQueueSet(priority);
addToScheduleQueue(queueSet,
new RecurringRateTaskWrapper(task, queueSet,
Clock.accurateForwardProgressingMillis() + initialDelay,
period));
}
/**
* Adds the ready TaskWrapper to the correct execute queue. Using the priority specified in the
* task, we pick the correct queue and add it.
* <p>
* If this is a scheduled or recurring task use {@link #addToScheduleQueue(TaskWrapper)}.
*
* @param task {@link TaskWrapper} to queue for the scheduler
*/
protected void addToExecuteQueue(QueueSet queueSet, OneTimeTaskWrapper task) {
if (workerPool.isShutdownStarted()) {
throw new RejectedExecutionException("Thread pool shutdown");
}
queueSet.addExecute(task);
}
/**
* Adds the ready TaskWrapper to the correct schedule queue. Using the priority specified in the
* task, we pick the correct queue and add it.
* <p>
* If this is just a single execution with no delay use {@link #addToExecuteQueue(OneTimeTaskWrapper)}.
*
* @param task {@link TaskWrapper} to queue for the scheduler
*/
protected void addToScheduleQueue(QueueSet queueSet, TaskWrapper task) {
if (workerPool.isShutdownStarted()) {
throw new RejectedExecutionException("Thread pool shutdown");
}
queueSet.addScheduled(task);
}
@Override
protected void finalize() {
// shutdown the thread pool so we don't leak threads if garbage collected
shutdown();
}
@Override
protected QueueManager getQueueManager() {
return taskQueueManager;
}
/**
* Class to manage the pool of worker threads. This class handles creating workers, storing
* them, and killing them once they are ready to expire. It also handles finding the
* appropriate worker when a task is ready to be executed.
*
* @since 3.5.0
*/
protected static class WorkerPool implements QueueSetListener {
protected final ThreadFactory threadFactory;
protected final Object poolSizeChangeLock;
protected final Object idleWorkerDequeLock;
protected final LongAdder idleWorkerCount;
protected final AtomicReference<Worker> idleWorker;
protected final AtomicInteger currentPoolSize;
protected final Object workerStopNotifyLock;
private final AtomicBoolean shutdownStarted;
private volatile boolean shutdownFinishing; // once true, never goes to false
private volatile int maxPoolSize; // can only be changed when poolSizeChangeLock locked
private volatile long workerTimedParkRunTime;
private QueueManager queueManager; // set before any threads started
protected WorkerPool(ThreadFactory threadFactory, int poolSize) {
ArgumentVerifier.assertGreaterThanZero(poolSize, "poolSize");
if (threadFactory == null) {
threadFactory = new ConfigurableThreadFactory(PriorityScheduler.class.getSimpleName() + "-", true);
}
poolSizeChangeLock = new Object();
idleWorkerDequeLock = new Object();
idleWorkerCount = new LongAdder();
idleWorker = new AtomicReference<>(null);
currentPoolSize = new AtomicInteger(0);
workerStopNotifyLock = new Object();
this.threadFactory = threadFactory;
this.maxPoolSize = poolSize;
this.workerTimedParkRunTime = Long.MAX_VALUE;
shutdownStarted = new AtomicBoolean(false);
shutdownFinishing = false;
}
/**
* Starts the pool, constructing the first thread to start consuming tasks (and starting other
* threads as appropriate). This should only be called once, and can NOT be called concurrently.
*
* @param queueManager QueueManager to source tasks for execution from
*/
public void start(QueueManager queueManager) {
if (currentPoolSize.get() != 0) {
throw new IllegalStateException();
}
this.queueManager = queueManager;
// this is to avoid a deficiency in workerIdle that could cause an idle thread to spin. This
// spin would be only if there is only one recurring task, and WHILE that recurring task is
// running. We solve this by adding this recurring task which wont run very long, and is
// scheduled to run very infrequently (Using Integer.MAX_VALUE that's every 24 days).
// we add this directly into the scheduledQueue structure to avoid having handleQueueUpdated
// invoked, and thus avoid starting any threads at this point.
InternalRunnable doNothingRunnable = new InternalRunnable() {
@Override
public void run() {
// nothing added here so that task runs as short as possible
// must be InternalRunnable, and not DoNothingRunnable so it's hidden from the task queue
}
};
queueManager.starvablePriorityQueueSet
.scheduleQueue.add(new RecurringRateTaskWrapper(doNothingRunnable,
queueManager.starvablePriorityQueueSet,
Clock.lastKnownForwardProgressingMillis() +
Integer.MAX_VALUE,
Integer.MAX_VALUE));
}
/**
* Checks if the shutdown has started by an invocation of {@link #startShutdown()}.
*
* @return {@code true} if the shutdown has started
*/
public boolean isShutdownStarted() {
return shutdownStarted.get();
}
/**
* Will start the shutdown of the worker pool.
*
* @return {@code true} if this call initiates the shutdown, {@code false} if the shutdown has already started
*/
public boolean startShutdown() {
return ! shutdownStarted.getAndSet(true);
}
/**
* Check weather the shutdown process is finished. In order for the shutdown to finish
* {@link #finishShutdown()} must have been invoked.
*
* @return {@code true} if the scheduler is finishing its shutdown
*/
public boolean isShutdownFinished() {
return shutdownFinishing;
}
/**
* Finishes the shutdown of the worker pool. This will ensure all finishing workers are
* killed.
*/
public void finishShutdown() {
shutdownFinishing = true;
addPoolStateChangeTask(new InternalRunnable() {
@Override
public void run() {
/* as long as we are continuing to run, we need to re-add ourself to ensure
* all threads are able to break out of task poll logic (once shutdown we stay shutdown)
*/
addPoolStateChangeTask(this);
}
});
}
/**
* When ever pool state is changed that is not checked in the pollTask loop (ie shutdown state,
* pool size), a task must be added that is continually added till the desired state is reached.
* <p>
* The purpose of this task is to break worker threads out of the tight loop for polling tasks,
* and instead get them to check the initial logic in {@link #workerIdle(Worker)} again. This
* is in contrast to putting the logic in the poll task loop, which only incurs a performance
* penalty.
*/
private void addPoolStateChangeTask(InternalRunnable task) {
// add to starvable queue, since we only need these tasks to be consumed and ran when there
// is nothing else to run.
queueManager.starvablePriorityQueueSet
.addExecute(new OneTimeTaskWrapper(task,
queueManager.starvablePriorityQueueSet.executeQueue,
Clock.lastKnownForwardProgressingMillis()));
}
/**
* Block until the thread pool has shutdown and all threads have been stopped. If neither
* {@link #shutdown()} or {@link #shutdownNow()} is invoked, then this will block until the
* timeout is reached.
*
* @param timeoutMillis time to block and wait for thread pool to shutdown
* @return {@code true} if the pool has shutdown, false if timeout was reached
* @throws InterruptedException Thrown if blocking thread is interrupted waiting for shutdown
*/
public boolean awaitTermination(long timeoutMillis) throws InterruptedException {
long start = timeoutMillis < Long.MAX_VALUE ?
Clock.accurateForwardProgressingMillis() : Clock.lastKnownForwardProgressingMillis();
synchronized (workerStopNotifyLock) {
long remainingMillis;
while ((! shutdownFinishing || currentPoolSize.get() > 0) &&
(remainingMillis = timeoutMillis - (Clock.lastKnownForwardProgressingMillis() - start)) > 0) {
workerStopNotifyLock.wait(remainingMillis);
}
}
return shutdownFinishing && currentPoolSize.get() == 0;
}
/**
* Getter for the currently set max worker pool size.
*
* @return current max pool size
*/
public int getMaxPoolSize() {
return maxPoolSize;
}
/**
* Change the set core pool size. If the value is less than the current max pool size, the max
* pool size will also be updated to this value.
* <p>
* If this was a reduction from the previous value, this call will examine idle workers to see
* if they should be expired. If this call reduced the max pool size, and the current running
* thread count is higher than the new max size, this call will NOT block till the pool is
* reduced. Instead as those workers complete, they will clean up on their own.
*
* @param newPoolSize New core pool size, must be at least one
*/
public void setPoolSize(int newPoolSize) {
ArgumentVerifier.assertGreaterThanZero(newPoolSize, "newPoolSize");
if (newPoolSize == maxPoolSize) {
// short cut the lock
return;
}
synchronized (poolSizeChangeLock) {
boolean poolSizeIncrease = newPoolSize > this.maxPoolSize;
this.maxPoolSize = newPoolSize;
if (poolSizeIncrease) {
// now that pool size increased, start a worker so workers we can for the waiting tasks
handleQueueUpdate();
} else {
addPoolStateChangeTask(new InternalRunnable() {
@Override
public void run() {
/* until the pool has reduced in size, we need to continue to add this task to
* wake threads out of the poll task loop
*/
if (currentPoolSize.get() > maxPoolSize) {
addPoolStateChangeTask(this);
}
}
});
}
}
}
/**
* Check for the current quantity of threads running in this pool (either active or idle).
*
* @return current thread count
*/
public int getCurrentPoolSize() {
return currentPoolSize.get();
}
/**
* Call to check how many workers are currently executing tasks.
*
* @return current number of workers executing tasks
*/
public int getActiveTaskCount() {
while (true) {
int poolSize = currentPoolSize.get();
int result = poolSize - idleWorkerCount.intValue();
if (poolSize == currentPoolSize.get()) {
return result;
}
}
}
/**
* Ensures all threads have been started. This will make new idle workers to accept tasks.
*/
public void prestartAllThreads() {
int casPoolSize;
while ((casPoolSize = currentPoolSize.get()) < maxPoolSize) {
if (currentPoolSize.compareAndSet(casPoolSize, casPoolSize + 1)) {
makeNewWorker();
}
}
}
/**
* This call creates and starts a new worker. It does not modify {@link currentPoolSize} so
* that MUST be updated in a thread safe way before this is invoked. As soon as the worker
* starts it will attempt to start taking tasks, no further action is needed.
*/
protected void makeNewWorker() {
Worker w = new Worker(this, threadFactory);
w.start();
}
/**
* Adds a worker to the head of the idle worker chain.
*
* @param worker Worker that is ready to become idle
*/
protected void addWorkerToIdleChain(Worker worker) {
idleWorkerCount.increment();
while (true) {
Worker casWorker = idleWorker.get();
// we can freely set this value until we get into the idle linked queue
worker.nextIdleWorker = casWorker;
if (idleWorker.compareAndSet(casWorker, worker)) {
break;
}
}
}
/**
* The counter part to {@link #addWorkerToIdleChain(Worker)}. This function has no safety
* checks. The worker provided MUST already be queued in the chain or problems will occur.
*
* @param worker Worker reference to remove from the chain (can not be {@code null})
*/
protected void removeWorkerFromIdleChain(Worker worker) {
idleWorkerCount.decrement();
/* We must lock here to avoid thread contention when removing from the chain. This is
* the one place where we set the reference to a workers "nextIdleWorker" from a thread
* outside of the workers thread. If we don't synchronize here, we may end up
* having workers disappear from the chain when the reference is nulled out.
*/
synchronized (idleWorkerDequeLock) {
Worker holdingWorker = idleWorker.get();
if (holdingWorker == worker) {
if (idleWorker.compareAndSet(worker, worker.nextIdleWorker)) {
return;
} else {
/* because we can only queue in parallel, we know that the conflict was a newly queued
* worker. In addition since we know that queued workers are added at the start, all
* that should be necessary is updating our holding worker reference
*/
holdingWorker = idleWorker.get();
}
}
// no need for null checks due to locking, we assume the worker is in the chain
while (holdingWorker.nextIdleWorker != worker) {
holdingWorker = holdingWorker.nextIdleWorker;
}
// now remove this worker from the chain
holdingWorker.nextIdleWorker = worker.nextIdleWorker;
// now out of the queue, lets clean up our reference
worker.nextIdleWorker = null;
}
}
/**
* Invoked when a worker becomes idle. This will provide another task for that worker, or
* block until a task is either ready, or the worker should be shutdown (either because pool
* was shut down, or max pool size changed).
*
* @param worker Worker which is now idle and ready for a task
* @return Task that is ready for immediate execution
*/
public TaskWrapper workerIdle(Worker worker) {
/* pool state checks, if any of these change we need a dummy task added to the queue to
* break out of the task polling loop below. This is done as an optimization, to avoid
* needing to check these on every loop (since they rarely change)
*/
while (true) {
int casPoolSize;
if (shutdownFinishing) {
currentPoolSize.decrementAndGet();
worker.stopIfRunning();
return null;
} else if ((casPoolSize = currentPoolSize.get()) > maxPoolSize) {
if (currentPoolSize.compareAndSet(casPoolSize, casPoolSize - 1)) {
worker.stopIfRunning();
return null;
}
}
// pool state is consistent, we should keep running
break;
}
boolean interruptedChecked = false;
boolean queued = false;
try {
while (true) {
TaskWrapper nextTask = queueManager.getNextTask();
if (nextTask == null) {
if (queued) {
// we can only park after we have queued, then checked again for a result
LockSupport.park();
worker.waitingForUnpark = false;
continue;
} else {
addWorkerToIdleChain(worker);
queued = true;
}
} else {
/* TODO - right now this has a a deficiency where a recurring period task can cut in
* the queue line. The condition would be as follows:
*
* * Thread 1 gets task to run...task is behind execution schedule, likely due to large queue
* * Thread 2 gets same task
* * Thread 1 gets reference, executes, task execution completes
* * Thread 2 now gets the reference, and execution check and time check pass fine
* * End result is that task has executed twice (on expected schedule), the second
* execution was unfair since it was done without respects to queue order and
* other tasks which are also likely behind execution schedule in this example
*
* This should be very rare, but is possible. The only way I see to solve this right
* now is to introduce locking.
*/
// must get executeReference before time is checked
short executeReference = nextTask.getExecuteReference();
long taskDelay = nextTask.getScheduleDelay();
if (taskDelay > 0) {
if (taskDelay == Long.MAX_VALUE) {
// the hack at construction/start is to avoid this from causing us to spin here
// if only one recurring task is scheduled (otherwise we would keep pulling
// that task while it's running)
continue;
}
if (queued) {
if (nextTask.getPureRunTime() < workerTimedParkRunTime) {
// we can only park after we have queued, then checked again for a result
workerTimedParkRunTime = nextTask.getPureRunTime();
LockSupport.parkNanos(Clock.NANOS_IN_MILLISECOND * taskDelay);
worker.waitingForUnpark = false;
workerTimedParkRunTime = Long.MAX_VALUE;
continue;
} else {
// there is another worker already doing a timed park, so we can wait till woken up
LockSupport.park();
worker.waitingForUnpark = false;
continue;
}
} else {
addWorkerToIdleChain(worker);
queued = true;
}
} else if (nextTask.canExecute(executeReference)) {
return nextTask;
}
}
// reset interrupted status if we may block and have not checked
if (queued && ! interruptedChecked) {
interruptedChecked = true;
if (Thread.interrupted()) {
// verify we were not interrupted due to pool shutdown
continue;
}
}
} // end pollTask loop
} finally {
// if queued, we must now remove ourselves, since worker is about to either shutdown or become active
if (queued) {
removeWorkerFromIdleChain(worker);
}
// wake up next worker so he can check if tasks are ready to consume
handleQueueUpdate();
if (! interruptedChecked) {
// reset interrupted status
Thread.interrupted();
}
}
}
@Override
public void handleQueueUpdate() {
while (true) {
Worker nextIdleWorker = idleWorker.get();
if (nextIdleWorker == null) {
int casSize = currentPoolSize.get();
if (casSize < maxPoolSize && ! shutdownFinishing) {
if (currentPoolSize.compareAndSet(casSize, casSize + 1)) {
// start a new worker for the next task
makeNewWorker();
break;
} // else loop and retry logic
} else {
// pool has all threads started, or is shutting down
break;
}
} else {
if (! nextIdleWorker.waitingForUnpark) {
nextIdleWorker.waitingForUnpark = true;
LockSupport.unpark(nextIdleWorker.thread);
}
break;
}
}
}
}
/**
* Runnable which will run on pool threads. It accepts runnables to run, and tracks usage.
*
* @since 1.0.0
*/
protected static class Worker extends AbstractService implements Runnable {
protected final WorkerPool workerPool;
protected final Thread thread;
protected volatile Worker nextIdleWorker;
protected volatile boolean waitingForUnpark;
protected Worker(WorkerPool workerPool, ThreadFactory threadFactory) {
this.workerPool = workerPool;
thread = threadFactory.newThread(this);
if (thread.isAlive()) {
throw new IllegalThreadStateException();
}
nextIdleWorker = null;
waitingForUnpark = false;
}
@Override
protected void startupService() {
thread.start();
}
@Override
protected void shutdownService() {
LockSupport.unpark(thread);
}
@Override
public void run() {
while (isRunning()) {
TaskWrapper nextTask = workerPool.workerIdle(this);
if (nextTask != null) { // may be null if we are shutting down
nextTask.runTask();
}
}
synchronized (workerPool.workerStopNotifyLock) {
workerPool.workerStopNotifyLock.notifyAll();
}
}
}
/**
* Runnable to be run after tasks already ready to execute. That way this can be submitted with
* a {@link #execute(Runnable)} to ensure that the shutdown is fair for tasks that were already
* ready to be run/executed. Once this runs the shutdown sequence will be finished, and no
* remaining asks in the queue can be executed.
*
* @since 1.0.0
*/
protected static class ShutdownRunnable implements InternalRunnable {
private final WorkerPool wm;
protected ShutdownRunnable(WorkerPool wm) {
this.wm = wm;
}
@Override
public void run() {
wm.finishShutdown();
}
}
}