package org.threadly.concurrent.wrapper;
import java.util.concurrent.Callable;
import org.threadly.concurrent.RunnableCallableAdapter;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.SameThreadSubmitterExecutor;
import org.threadly.concurrent.SubmitterScheduler;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.concurrent.future.ListenableRunnableFuture;
import org.threadly.concurrent.lock.StripedLock;
import org.threadly.util.ArgumentVerifier;
/**
* This is a class which is more full featured than {@link KeyDistributedExecutor}, but it does
* require a scheduler implementation in order to be able to perform scheduling.
* <p>
* The same guarantees and restrictions for the {@link KeyDistributedExecutor} also exist for this
* class. Please read the javadoc for {@link KeyDistributedExecutor} to understand more about how
* this operates.
*
* @since 4.6.0 (since 1.0.0 as org.threadly.concurrent.TaskSchedulerDistributor)
*/
public class KeyDistributedScheduler extends KeyDistributedExecutor {
protected final SubmitterScheduler scheduler;
/**
* Constructor to use a provided scheduler implementation for running tasks.
* <p>
* This constructs with a default expected level of concurrency of 16. This also does not
* attempt to have an accurate queue size for the {@link #getTaskQueueSize(Object)} call (thus
* preferring high performance).
*
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
*/
public KeyDistributedScheduler(SubmitterScheduler scheduler) {
this(DEFAULT_LOCK_PARALISM, scheduler, Integer.MAX_VALUE, false);
}
/**
* Constructor to use a provided executor implementation for running tasks.
* <p>
* This constructor allows you to specify if you want accurate queue sizes to be tracked for
* given thread keys. There is a performance hit associated with this, so this should only be
* enabled if {@link #getTaskQueueSize(Object)} calls will be used.
* <p>
* This constructs with a default expected level of concurrency of 16.
*
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param accurateQueueSize {@code true} to make {@link #getTaskQueueSize(Object)} more accurate
*/
public KeyDistributedScheduler(SubmitterScheduler scheduler, boolean accurateQueueSize) {
this(DEFAULT_LOCK_PARALISM, scheduler, Integer.MAX_VALUE, accurateQueueSize);
}
/**
* Constructor to use a provided executor implementation for running tasks.
* <p>
* This constructor allows you to provide a maximum number of tasks for a key before it yields
* to another key. This can make it more fair, and make it so no single key can starve other
* keys from running. The lower this is set however, the less efficient it becomes in part
* because it has to give up the thread and get it again, but also because it must copy the
* subset of the task queue which it can run.
* <p>
* This constructs with a default expected level of concurrency of 16. This also does not
* attempt to have an accurate queue size for the {@link #getTaskQueueSize(Object)} call (thus
* preferring high performance).
*
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param maxTasksPerCycle maximum tasks run per key before yielding for other keys
*/
public KeyDistributedScheduler(SubmitterScheduler scheduler, int maxTasksPerCycle) {
this(DEFAULT_LOCK_PARALISM, scheduler, maxTasksPerCycle, false);
}
/**
* Constructor to use a provided executor implementation for running tasks.
* <p>
* This constructor allows you to provide a maximum number of tasks for a key before it yields
* to another key. This can make it more fair, and make it so no single key can starve other
* keys from running. The lower this is set however, the less efficient it becomes in part
* because it has to give up the thread and get it again, but also because it must copy the
* subset of the task queue which it can run.
* <p>
* This also allows you to specify if you want accurate queue sizes to be tracked for given
* thread keys. There is a performance hit associated with this, so this should only be enabled
* if {@link #getTaskQueueSize(Object)} calls will be used.
* <p>
* This constructs with a default expected level of concurrency of 16.
*
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param maxTasksPerCycle maximum tasks run per key before yielding for other keys
* @param accurateQueueSize {@code true} to make {@link #getTaskQueueSize(Object)} more accurate
*/
public KeyDistributedScheduler(SubmitterScheduler scheduler, int maxTasksPerCycle,
boolean accurateQueueSize) {
this(DEFAULT_LOCK_PARALISM, scheduler, maxTasksPerCycle, accurateQueueSize);
}
/**
* Constructor to use a provided scheduler implementation for running tasks.
* <p>
* This constructor does not attempt to have an accurate queue size for the
* {@link #getTaskQueueSize(Object)} call (thus preferring high performance).
* <p>
* The parallelism value should be a factor of how many keys are submitted to the pool during any
* given period of time. Depending on task execution duration, and quantity of threads executing
* tasks this value may be able to be smaller than expected. Higher values result in less lock
* contention, but more memory usage. Most systems will run fine with this anywhere from 4 to 64.
*
* @param expectedParallism level of expected quantity of threads adding tasks in parallel
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
*/
public KeyDistributedScheduler(int expectedParallism, SubmitterScheduler scheduler) {
this(expectedParallism, scheduler, Integer.MAX_VALUE, false);
}
/**
* Constructor to use a provided scheduler implementation for running tasks.
* <p>
* This constructor allows you to specify if you want accurate queue sizes to be tracked for
* given thread keys. There is a performance hit associated with this, so this should only be
* enabled if {@link #getTaskQueueSize(Object)} calls will be used.
* <p>
* The parallelism value should be a factor of how many keys are submitted to the pool during any
* given period of time. Depending on task execution duration, and quantity of threads executing
* tasks this value may be able to be smaller than expected. Higher values result in less lock
* contention, but more memory usage. Most systems will run fine with this anywhere from 4 to 64.
*
* @param expectedParallism level of expected quantity of threads adding tasks in parallel
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param accurateQueueSize {@code true} to make {@link #getTaskQueueSize(Object)} more accurate
*/
public KeyDistributedScheduler(int expectedParallism, SubmitterScheduler scheduler,
boolean accurateQueueSize) {
this(expectedParallism, scheduler, Integer.MAX_VALUE, accurateQueueSize);
}
/**
* Constructor to use a provided scheduler implementation for running tasks.
* <p>
* This constructor allows you to provide a maximum number of tasks for a key before it yields
* to another key. This can make it more fair, and make it so no single key can starve other
* keys from running. The lower this is set however, the less efficient it becomes in part
* because it has to give up the thread and get it again, but also because it must copy the
* subset of the task queue which it can run.
* <p>
* This constructor does not attempt to have an accurate queue size for the
* {@link #getTaskQueueSize(Object)} call (thus preferring high performance).
* <p>
* The parallelism value should be a factor of how many keys are submitted to the pool during any
* given period of time. Depending on task execution duration, and quantity of threads executing
* tasks this value may be able to be smaller than expected. Higher values result in less lock
* contention, but more memory usage. Most systems will run fine with this anywhere from 4 to 64.
*
* @param expectedParallism level of expected quantity of threads adding tasks in parallel
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param maxTasksPerCycle maximum tasks run per key before yielding for other keys
*/
public KeyDistributedScheduler(int expectedParallism, SubmitterScheduler scheduler,
int maxTasksPerCycle) {
this(expectedParallism, scheduler, maxTasksPerCycle, false);
}
/**
* Constructor to use a provided scheduler implementation for running tasks.
* <p>
* This constructor allows you to provide a maximum number of tasks for a key before it yields
* to another key. This can make it more fair, and make it so no single key can starve other
* keys from running. The lower this is set however, the less efficient it becomes in part
* because it has to give up the thread and get it again, but also because it must copy the
* subset of the task queue which it can run.
* <p>
* This also allows you to specify if you want accurate queue sizes to be tracked for given
* thread keys. There is a performance hit associated with this, so this should only be enabled
* if {@link #getTaskQueueSize(Object)} calls will be used.
* <p>
* The parallelism value should be a factor of how many keys are submitted to the pool during any
* given period of time. Depending on task execution duration, and quantity of threads executing
* tasks this value may be able to be smaller than expected. Higher values result in less lock
* contention, but more memory usage. Most systems will run fine with this anywhere from 4 to 64.
*
* @param expectedParallism level of expected quantity of threads adding tasks in parallel
* @param scheduler A multi-threaded scheduler to distribute tasks to. Ideally has as many
* possible threads as keys that will be used in parallel.
* @param maxTasksPerCycle maximum tasks run per key before yielding for other keys
* @param accurateQueueSize {@code true} to make {@link #getTaskQueueSize(Object)} more accurate
*/
public KeyDistributedScheduler(int expectedParallism, SubmitterScheduler scheduler,
int maxTasksPerCycle, boolean accurateQueueSize) {
this(scheduler, new StripedLock(expectedParallism), maxTasksPerCycle, accurateQueueSize);
}
/**
* Constructor to be used in unit tests.
* <p>
* This constructor allows you to provide a maximum number of tasks for a key before it yields
* to another key. This can make it more fair, and make it so no single key can starve other
* keys from running. The lower this is set however, the less efficient it becomes in part
* because it has to give up the thread and get it again, but also because it must copy the
* subset of the task queue which it can run.
*
* @param scheduler scheduler to be used for task worker execution
* @param sLock lock to be used for controlling access to workers
* @param maxTasksPerCycle maximum tasks run per key before yielding for other keys
*/
protected KeyDistributedScheduler(SubmitterScheduler scheduler, StripedLock sLock,
int maxTasksPerCycle, boolean accurateQueueSize) {
super(scheduler, sLock, maxTasksPerCycle, accurateQueueSize);
this.scheduler = scheduler;
}
/**
* Returns a scheduler implementation where all tasks submitted on this scheduler will run on
* the provided key.
*
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @return scheduler which will only execute based on the provided key
*/
public SubmitterScheduler getSchedulerForKey(Object threadKey) {
ArgumentVerifier.assertNotNull(threadKey, "threadKey");
return new KeyScheduler(threadKey);
}
/**
* Schedule a one time task with a given delay that will not run concurrently based off the
* thread key.
*
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task Task to execute
* @param delayInMs Time to wait to execute task
*/
public void schedule(Object threadKey, Runnable task, long delayInMs) {
ArgumentVerifier.assertNotNull(threadKey, "threadKey");
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(delayInMs, "delayInMs");
if (delayInMs == 0) {
addTask(threadKey, task, executor);
} else {
scheduler.schedule(new AddTask(threadKey, task), delayInMs);
}
}
/**
* Schedule a task with a given delay. There is a slight increase in load when using
* {@link #submitScheduled(Object, Runnable, long)} over
* {@link #schedule(Object, Runnable, long)}. So this should only be used when the future
* is necessary.
* <p>
* The {@link ListenableFuture#get()} method will return {@code null} once the runnable has completed.
*
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task runnable to execute
* @param delayInMs time in milliseconds to wait to execute task
* @return a future to know when the task has completed
*/
public ListenableFuture<?> submitScheduled(Object threadKey, Runnable task, long delayInMs) {
return submitScheduled(threadKey, task, null, delayInMs);
}
/**
* Schedule a task with a given delay. The future {@link ListenableFuture#get()} method will
* return null once the runnable has completed.
*
* @param <T> type of result returned from the future
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task runnable to execute
* @param result result to be returned from resulting {@link ListenableFuture#get()} when runnable completes
* @param delayInMs time in milliseconds to wait to execute task
* @return a future to know when the task has completed
*/
public <T> ListenableFuture<T> submitScheduled(Object threadKey, Runnable task,
T result, long delayInMs) {
return submitScheduled(threadKey, new RunnableCallableAdapter<>(task, result), delayInMs);
}
/**
* Schedule a {@link Callable} with a given delay. This is needed when a result needs to be
* consumed from the callable.
*
* @param <T> type of result returned from the future
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task callable to be executed
* @param delayInMs time in milliseconds to wait to execute task
* @return a future to know when the task has completed and get the result of the callable
*/
public <T> ListenableFuture<T> submitScheduled(Object threadKey,
Callable<T> task, long delayInMs) {
ArgumentVerifier.assertNotNull(threadKey, "threadKey");
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(delayInMs, "delayInMs");
ListenableRunnableFuture<T> rf = new ListenableFutureTask<>(false, task);
if (delayInMs == 0) {
addTask(threadKey, rf, executor);
} else {
scheduler.schedule(new AddTask(threadKey, rf), delayInMs);
}
return rf;
}
/**
* Schedule a fixed delay recurring task to run. The recurring delay time will be from the
* point where execution has finished. So the execution frequency is the
* {@code recurringDelay + runtime} for the provided task.
*
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task Task to be executed.
* @param initialDelay Delay in milliseconds until first run.
* @param recurringDelay Delay in milliseconds for running task after last finish.
*/
public void scheduleTaskWithFixedDelay(Object threadKey, Runnable task,
long initialDelay, long recurringDelay) {
ArgumentVerifier.assertNotNull(threadKey, "threadKey");
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay");
ArgumentVerifier.assertNotNegative(recurringDelay, "recurringDelay");
RecrringDelayTask rdt = new RecrringDelayTask(threadKey, task, recurringDelay);
if (initialDelay == 0) {
addTask(threadKey, rdt, executor);
} else {
scheduler.schedule(new AddTask(threadKey, rdt), initialDelay);
}
}
/**
* Schedule a fixed rate recurring task to run. The recurring delay will be the same,
* regardless of how long task execution takes. A given runnable will not run concurrently
* (unless it is submitted to the scheduler multiple times). Instead of execution takes longer
* than the period, the next run will occur immediately (given thread availability in the pool).
*
* @param threadKey object key where {@code equals()} will be used to determine execution thread
* @param task runnable to be executed
* @param initialDelay delay in milliseconds until first run
* @param period amount of time in milliseconds between the start of recurring executions
*/
public void scheduleTaskAtFixedRate(Object threadKey, Runnable task,
long initialDelay, long period) {
ArgumentVerifier.assertNotNull(threadKey, "threadKey");
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay");
ArgumentVerifier.assertGreaterThanZero(period, "period");
RecrringRateTask rrt = new RecrringRateTask(threadKey, task, period);
if (initialDelay == 0) {
addTask(threadKey, rrt, executor);
} else {
scheduler.schedule(new AddTask(threadKey, rrt), initialDelay);
}
}
/**
* Task which will run delayed to add a task into the queue when ready.
*
* @since 1.0.0
*/
protected class AddTask implements Runnable, RunnableContainer {
protected final Object key;
protected final Runnable task;
protected AddTask(Object key, Runnable task) {
this.key = key;
this.task = task;
}
@Override
public void run() {
addTask(key, task, SameThreadSubmitterExecutor.instance());
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
/**
* Container for runnables which run with a fixed delay after the previous run.
*
* @since 3.1.0
*/
protected class RecrringDelayTask implements Runnable, RunnableContainer {
protected final Object key;
protected final Runnable task;
protected final long recurringDelay;
protected RecrringDelayTask(Object key, Runnable task, long recurringDelay) {
this.key = key;
this.task = task;
this.recurringDelay = recurringDelay;
}
@Override
public void run() {
try {
task.run();
} finally {
scheduler.schedule(new AddTask(key, this), recurringDelay);
}
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
/**
* Container for runnables which run with a fixed rate, regardless of execution time.
*
* @since 3.1.0
*/
protected class RecrringRateTask implements Runnable, RunnableContainer {
protected final Object key;
protected final Runnable task;
protected final long recurringPeriod;
protected RecrringRateTask(Object key, Runnable task, long recurringPeriod) {
this.key = key;
this.task = task;
this.recurringPeriod = recurringPeriod;
}
@Override
public void run() {
scheduler.schedule(new AddTask(key, this), recurringPeriod);
task.run();
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
/**
* Simple simple scheduler implementation that submits executions and scheduling on a given key.
*
* @since 2.5.0
*/
protected class KeyScheduler extends KeySubmitter implements SubmitterScheduler {
protected KeyScheduler(Object threadKey) {
super(threadKey);
}
@Override
public void schedule(Runnable task, long delayInMs) {
KeyDistributedScheduler.this.schedule(threadKey, task, delayInMs);
}
@Override
public ListenableFuture<?> submitScheduled(Runnable task, long delayInMs) {
return submitScheduled(task, null, delayInMs);
}
@Override
public <T> ListenableFuture<T> submitScheduled(Runnable task, T result, long delayInMs) {
return KeyDistributedScheduler.this.submitScheduled(threadKey, task, result, delayInMs);
}
@Override
public <T> ListenableFuture<T> submitScheduled(Callable<T> task, long delayInMs) {
return KeyDistributedScheduler.this.submitScheduled(threadKey, task, delayInMs);
}
@Override
public void scheduleWithFixedDelay(Runnable task, long initialDelay, long recurringDelay) {
KeyDistributedScheduler.this.scheduleTaskWithFixedDelay(threadKey, task,
initialDelay, recurringDelay);
}
@Override
public void scheduleAtFixedRate(Runnable task, long initialDelay, long period) {
KeyDistributedScheduler.this.scheduleTaskAtFixedRate(threadKey, task, initialDelay, period);
}
}
}