package org.threadly.concurrent; import java.util.concurrent.atomic.AtomicInteger; import org.threadly.util.ArgumentVerifier; /** * Abstract implementation for more complicated recurring behavior. Unlike submitting a task to * {@link SubmitterScheduler#scheduleWithFixedDelay(Runnable, long, long)}, this can provide the * ability to only have the task scheduled if there is work to do. In addition it provides the * ability to change the frequency of execution without needing to remove and re-add the task. * <p> * This task will only schedule or reschedule itself if it has been notified there is work to do. * It is assumed that after execution all work is complete. If there is additional work to * perform, then the task should invoke {@link #signalToRun()} before it completes to ensure that * it is rescheduled at the current set delay. Because of that behavior there is no way to remove * this task from the scheduler, instead you must just ensure that {@link #signalToRun()} is not * invoked to prevent the task from rescheduling itself. * <p> * An additional advantage to using this over scheduling a recurring task is that you don't have * to worry about removing the task before garbage collection occurs (ie no cleanup, just stop * invoking {@link #signalToRun()}). * * @since 4.9.0 */ public abstract class ReschedulingOperation { private final SubmitterScheduler scheduler; // -1 = not scheduled, 0 = scheduled, 1 = running, 2 = updated while running private final AtomicInteger taskState; private final CheckRunner runner; private volatile long scheduleDelay; /** * Construct a new operation with the provided scheduler to schedule on to and the initial delay. * * @param scheduler Scheduler to execute on. * @param scheduleDelay Delay in milliseconds to schedule operation out when has stuff to do */ protected ReschedulingOperation(SubmitterScheduler scheduler, long scheduleDelay) { ArgumentVerifier.assertNotNull(scheduler, "scheduler"); this.scheduler = scheduler; this.taskState = new AtomicInteger(-1); this.runner = new CheckRunner(); setScheduleDelay(scheduleDelay); } /** * Check to see if this operation is either currently running, or scheduled to run. * * @return {@code true} means this operation still has things to do */ public boolean isActive() { return taskState.get() != -1; } /** * Adjusts the delay at which this task is scheduled or rescheduled. This can be invoked during * {@link #run()} to change how long till it executes next. * * @param scheduleDelay Delay in milliseconds to schedule operation out on, can not be negative */ public void setScheduleDelay(long scheduleDelay) { ArgumentVerifier.assertNotNegative(scheduleDelay, "scheduleDelay"); this.scheduleDelay = scheduleDelay; } /** * Invoke to indicate that this operation has stuff to do. If necessary the task will be * scheduled for execution. If the task is already running then it will ensure the task * re-executes itself when done (at the set delay). This re-execution can help ensure that any * thread state changes can be witnessed on the next execution. */ public void signalToRun() { while (true) { if (taskState.get() == -1) { if (taskState.compareAndSet(-1, 0)) { scheduler.schedule(runner, scheduleDelay); return; } } else if (taskState.get() == 1) { if (taskState.compareAndSet(1, 2)) { return; } } else { // either already scheduled, or already marked as more added return; } } } /** * Abstract function which must be implemented to handle actual operation. It is expected that * when this runs all outstanding work is handled. If it can not be fully handled then invoke * {@link #signalToRun()} before returning. * <p> * If this throws an exception it will not impact the state of future executions (ie if * {@link #signalToRun()} was invoked, the task will be rescheduled despite a thrown exception). */ protected abstract void run(); /** * Class to in a thread safe way update the execution state, and reschedule the task on * completion if necessary. * * @since 4.9.0 */ protected class CheckRunner implements Runnable { @Override public void run() { taskState.set(1); try { ReschedulingOperation.this.run(); } finally { while (true) { if (taskState.get() == 1) { if (taskState.compareAndSet(1, -1)) { // nothing to run, just return break; } } else if (taskState.get() == 2) { // will be set back to 1 when this restarts scheduler.schedule(this, scheduleDelay); break; } } } } @Override public String toString() { return "CheckRunner for: " + ReschedulingOperation.this.toString(); } } }