package org.threadly.test.concurrent; import java.util.List; import org.threadly.concurrent.AbstractPriorityScheduler; import org.threadly.concurrent.NoThreadScheduler; import org.threadly.concurrent.TaskPriority; import org.threadly.util.Clock; import org.threadly.util.ExceptionHandler; /** * This differs from {@link org.threadly.concurrent.NoThreadScheduler} in that time is ONLY * advanced via the tick calls. That means that if you schedule a task, it will be scheduled off * of either the creation time, or the last tick time, what ever the most recent point is. This * allows you to progress time forward faster than it could in real time, having tasks execute * faster, etc, etc. * <p> * The tasks in this scheduler are only progressed forward with calls to {@link #tick()}. Since * it is running on the calling thread, calls to {@code Object.wait()} and {@code Thread.sleep()} * from sub tasks will block (possibly forever). The call to {@link #tick()} will not unblock * till there is no more work for the scheduler to currently handle. * * @since 2.0.0 */ public class TestableScheduler extends AbstractPriorityScheduler { private final InternalScheduler scheduler; private long nowInMillis; /** * Constructs a new {@link TestableScheduler} scheduler. */ public TestableScheduler() { this(null, DEFAULT_LOW_PRIORITY_MAX_WAIT_IN_MS); } /** * Constructs a new {@link TestableScheduler} scheduler. * * @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 TestableScheduler(TaskPriority defaultPriority, long maxWaitForLowPriorityInMs) { super(defaultPriority); scheduler = new InternalScheduler(defaultPriority, maxWaitForLowPriorityInMs); nowInMillis = Clock.lastKnownTimeMillis(); } @Override public boolean isShutdown() { return scheduler.isShutdown(); } @Override public int getActiveTaskCount() { return scheduler.getActiveTaskCount(); } @Override public void scheduleWithFixedDelay(Runnable task, long initialDelay, long recurringDelay, TaskPriority priority) { scheduler.scheduleWithFixedDelay(task, initialDelay, recurringDelay, priority); } @Override public void scheduleAtFixedRate(Runnable task, long initialDelay, long period, TaskPriority priority) { scheduler.scheduleAtFixedRate(task, initialDelay, period, priority); } @Override protected OneTimeTaskWrapper doSchedule(Runnable task, long delayInMillis, TaskPriority priority) { return scheduler.doSchedule(task, delayInMillis, priority); } @Override protected QueueManager getQueueManager() { return scheduler.getQueueManager(); } /** * Returns the last provided time to the tick call. If tick has not been called yet, then this * will represent the time at construction. * * @return last time the scheduler used for reference on execution */ public long getLastTickTime() { return nowInMillis; } /** * This is to provide a convince when advancing the scheduler forward an explicit amount of time. * Where tick accepts an absolute time, this accepts an amount of time to advance forward. That * way the user does not have to track the current time. * * @param timeInMillis amount in milliseconds to advance the scheduler forward * @return quantity of tasks run during this tick call */ public int advance(long timeInMillis) { return advance(timeInMillis, null); } /** * This is to provide a convince when advancing the scheduler forward an explicit amount of time. * Where tick accepts an absolute time, this accepts an amount of time to advance forward. That * way the user does not have to track the current time. * <p> * This call allows you to specify an {@link ExceptionHandler}. If provided, if any tasks throw * an exception, this will be called to inform them of the exception. This allows you to ensure * that you get a returned task count (meaning if provided, no exceptions except a possible * {@link InterruptedException} can be thrown). If {@code null} is provided for the exception * handler, than any tasks which throw a {@link RuntimeException}, will throw out of this * invocation. * * @since 3.2.0 * @param timeInMillis amount in milliseconds to advance the scheduler forward * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @return quantity of tasks run during this tick call */ public int advance(long timeInMillis, ExceptionHandler exceptionHandler) { return tick(nowInMillis + timeInMillis, exceptionHandler); } /** * Progresses tasks for the current time. This will block as it runs as many scheduled or * waiting tasks as possible. This call will NOT block if no task are currently ready to run. * <p> * If any tasks throw a {@link RuntimeException}, they will be bubbled up to this tick call. * Any tasks past that task will not run till the next call to tick. So it is important that * the implementor handle those exceptions. * * @return quantity of tasks run during this tick call */ public int tick() { return tick(null); } /** * Progresses tasks for the current time. This will block as it runs as many scheduled or * waiting tasks as possible. This call will NOT block if no task are currently ready to run. * <p> * This call allows you to specify an {@link ExceptionHandler}. If provided, if any tasks throw * an exception, this will be called to inform them of the exception. This allows you to ensure * that you get a returned task count (meaning if provided, no exceptions except a possible * {@link InterruptedException} can be thrown). If {@code null} is provided for the exception * handler, than any tasks which throw a {@link RuntimeException}, will throw out of this * invocation. * * @since 3.2.0 * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @return quantity of tasks run during this tick call */ public int tick(ExceptionHandler exceptionHandler) { long currentRealTime = Clock.accurateTimeMillis(); if (nowInMillis > currentRealTime) { return tick(nowInMillis, exceptionHandler); } else { return tick(currentRealTime, exceptionHandler); } } /** * This progresses tasks based off the time provided. This is primarily used in testing by * providing a possible time in the future (to execute future tasks). This call will NOT block * if no task are currently ready to run. * <p> * If any tasks throw a {@link RuntimeException}, they will be bubbled up to this tick call. * Any tasks past that task will not run till the next call to tick. So it is important that * the implementor handle those exceptions. * <p> * This call accepts the absolute time in milliseconds. If you want to advance the scheduler a * specific amount of time forward, look at the "advance" call. * * @param currentTime Absolute time to provide for looking at task run time * @return quantity of tasks run in this tick call */ public int tick(long currentTime) { return tick(currentTime, null); } /** * This progresses tasks based off the time provided. This is primarily used in testing by * providing a possible time in the future (to execute future tasks). This call will NOT block * if no task are currently ready to run. * <p> * This call allows you to specify an {@link ExceptionHandler}. If provided, if any tasks throw * an exception, this will be called to inform them of the exception. This allows you to ensure * that you get a returned task count (meaning if provided, no exceptions except a possible * {@link InterruptedException} can be thrown). If {@code null} is provided for the exception * handler, than any tasks which throw a {@link RuntimeException}, will throw out of this * invocation. * <p> * This call accepts the absolute time in milliseconds. If you want to advance the scheduler a * specific amount of time forward, look at the "advance" call. * * @since 3.2.0 * @param currentTime Absolute time to provide for looking at task run time * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @return quantity of tasks run in this tick call */ public int tick(long currentTime, ExceptionHandler exceptionHandler) { if (nowInMillis > currentTime) { throw new IllegalArgumentException("Time can not go backwards"); } nowInMillis = currentTime; return scheduler.tick(exceptionHandler); } /** * Checks if there are tasks ready to be run on the scheduler. If * {@link #tick(ExceptionHandler)} is not currently being called, this call indicates if the * next {@link #tick(ExceptionHandler)} will have at least one task to run. If * {@link #tick(ExceptionHandler)} is currently being invoked, this call will do a best attempt * to indicate if there is at least one more task to run (not including the task which may * currently be running). It's a best attempt as it will try not to block the thread invoking * {@link #tick(ExceptionHandler)} to prevent it from accepting additional work. * * @return {@code true} if there are task waiting to run */ public boolean hasTaskReadyToRun() { return scheduler.hasTaskReadyToRun(); } /** * Checks how long till the next task will be ready to execute. If there are no tasks in this * scheduler currently then {@link Long#MAX_VALUE} will be returned. If there is a task ready * to execute this will return a value less than or equal to zero. If the task is past its * desired point of execution the result will be a negative amount of milliseconds past that * point in time. * <p> * Generally this is called from the same thread that would invoke * {@link #tick(ExceptionHandler)} (but does not have to be). Since this does not block or lock * if being invoked in parallel with {@link #tick(ExceptionHandler)}, the results may be no * longer accurate by the time this invocation has returned. * <p> * This can be useful if you want to know how long you can block on something, ASSUMING you can * detect that something has been added to the scheduler, and interrupt that blocking in order * to handle tasks. * * @return Milliseconds till the next task is ready to run */ public long getDelayTillNextTask() { return scheduler.getDelayTillNextTask(); } /** * Removes any tasks waiting to be run. Will not interrupt any tasks currently running if * {@link #tick(ExceptionHandler)} is being called. But will avoid additional tasks from being * run on the current {@link #tick(ExceptionHandler)} call. * <p> * If tasks are added concurrently during this invocation they may or may not be removed. * * @return List of runnables which were waiting in the task queue to be executed (and were now removed) */ public List<Runnable> clearTasks() { return scheduler.clearTasks(); } /** * Small internal wrapper class so that we can control what from the "NoThreadScheduler" api's * we want to expose from this implementation. * * @since 2.4.0 */ private class InternalScheduler extends NoThreadScheduler { public InternalScheduler(TaskPriority defaultPriority, long maxWaitForLowPriorityInMs) { super(defaultPriority, maxWaitForLowPriorityInMs); } @Override protected long nowInMillis(boolean accurate) { return nowInMillis; } @Override protected QueueManager getQueueManager() { return super.getQueueManager(); } @Override protected OneTimeTaskWrapper doSchedule(Runnable task, long delayInMillis, TaskPriority priority) { return super.doSchedule(task, delayInMillis, priority); } } }