com.digitalcipher.spiked.timing.ScheduledTask Maven / Gradle / Ivy
package com.digitalcipher.spiked.timing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import static com.digitalcipher.spiked.timing.ScheduleType.FIXED_DELAY;
import static com.digitalcipher.spiked.timing.ScheduleType.FIXED_RATE;
/**
* Scheduled task that maintains its execution status state.
*
* @param The return type of the task
*/
@SuppressWarnings("WeakerAccess")
public class ScheduledTask extends CompletableFuture {
private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);
private final long id = System.nanoTime();
private final int wheelOffset;
private final int periodicWheelOffset;
private final int periodicTimesAround;
private final AtomicInteger remainingTimesAround = new AtomicInteger();
private final Supplier task;
private final ScheduleType scheduleType;
private final BiConsumer, Long> rescheduling;
private final ExecutorService executorService;
/**
* @param timesAround The number of times around the timer the cursor needs to move before the task is executed
* @param wheelOffset The offset in the wheel, from the current cursor, for the bucket that holds the task
* @param periodicTimesAround The number of times around the timer the cursor needs to move before executing
* the period task, after the initial delay.
* @param periodicWheelOffset The offset in the wheel, from the current cursor, for the bucket that holds the
* periodic task, after the initial delay.
* @param timeout The timeout for tasks on a fixed delay schedule
* @param task The task to execute
* @param scheduleType The schedule type (i.e. one-shot, fixed-rate, fixed-delay)
* @param rescheduling The reschedule callback that tells the hashed-wheel-timer to reschedule
* @param executorService The executor service for running the task
*/
private ScheduledTask(final int timesAround,
final int wheelOffset,
final int periodicTimesAround,
final int periodicWheelOffset,
final Duration timeout,
final Supplier task,
final ScheduleType scheduleType,
final BiConsumer, Long> rescheduling,
final ExecutorService executorService) {
this.periodicWheelOffset = periodicWheelOffset;
this.periodicTimesAround = periodicTimesAround;
this.remainingTimesAround.set(timesAround);
this.wheelOffset = wheelOffset;
this.task = task;
this.scheduleType = scheduleType;
this.rescheduling = rescheduling;
this.executorService = executorService;
// set a timer to shut down the periodic tasks
if(Objects.nonNull(timeout)) {
final ScheduledExecutorService taskCompleteExecutor = Executors.newSingleThreadScheduledExecutor();
taskCompleteExecutor.schedule(() -> {
complete(null);
taskCompleteExecutor.shutdown();
cancel(true);
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
}
}
/**
* @param The return type from the task
* @return A validated {@link ScheduledTask} instance
*/
public static Builder builder() {
return new Builder<>();
}
/**
* @return The wheel offset for the initial delay
*/
public int wheelOffset() {
return wheelOffset;
}
/**
* @return The wheel offset for the periodic delay
*/
public int periodicWheelOffset() {
return periodicWheelOffset;
}
/**
* Process the scheduled tasks. Updates the number of times around, and submits the task for execution
* if it the cursor has gone around enough times. For periodic tasks, updates the times-around
* and calls the hashed-while-timer to reschedule the task.
*
* @return A reference to this instance for periodic tasks; null for one-shot or cancelled tasks
*/
ScheduledTask process() {
// return null for cancelled tasks
if(isCancelled() || isDone()) {
return null;
}
// execute tasks that are ready
if(remainingTimesAround.decrementAndGet() < 0) {
switch(scheduleType) {
case ONE_SHOT:
executorService.submit(() -> complete(task.get()));
return null;
case FIXED_DELAY: {
// wait for the task to complete, and then proceed to the fix-rate logic
task.get();
final long start = System.nanoTime();
remainingTimesAround.set(periodicTimesAround);
rescheduling.accept(this, start);
return null;
}
case FIXED_RATE: {
// approx. 40 to 60 µs on average
final long start = System.nanoTime();
executorService.submit(task::get);
remainingTimesAround.set(periodicTimesAround);
rescheduling.accept(this, start);
return null;
}
}
}
// do nothing and return this unchanged
return this;
}
/**
* Submits the task for execution. For periodic tasks, updates the time-around and calls the reschedule
* function.
*
* @return A reference to this instance
*/
ScheduledTask executeNow() {
switch(scheduleType) {
case ONE_SHOT:
executorService.submit(() -> complete(task.get()));
return this;
case FIXED_DELAY:
// wait for the task to complete, and then proceed to the fix-rate logic
task.get();
remainingTimesAround.set(periodicTimesAround);
rescheduling.accept(this, System.nanoTime());
return this;
case FIXED_RATE:
executorService.submit(task::get);
remainingTimesAround.set(periodicTimesAround);
rescheduling.accept(this, System.nanoTime());
return this;
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(final Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
final ScheduledTask> that = (ScheduledTask>) o;
return id == that.id;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hashCode(id);
}
/**
* Builder for constructing a validated {@link ScheduledTask} instance
*
* @param The return type of the task
*/
public static class Builder {
private Integer timesAround;
private Integer wheelOffset;
private Integer periodicWheelOffset;
private Integer periodicTimesAround;
private Duration timeout;
private Supplier task;
private ScheduleType scheduleType;
private BiConsumer, Long> rescheduling;
private ExecutorService executorService;
/**
* Required for all schedule types.
* Sets the initial delay values
*
* @param timesAround The number of times around the timer the cursor needs to move before the task is executed
* @param wheelOffset The offset in the wheel, from the current cursor, for the bucket that holds the task
* @return A reference to this builder for chaining
*/
public Builder withInitialDelayInfo(final int timesAround, final int wheelOffset) {
this.wheelOffset = wheelOffset;
this.timesAround = timesAround;
return this;
}
/**
* Required only for periodic schedules (i.e. fixed-rate, fixed-delay.
* Sets the function called when periodic tasks need to be rescheduled after executing.
*
* @param rescheduling The rescheduling function.
* @return A reference to this builder for chaining
*/
public Builder withRescheduling(final BiConsumer, Long> rescheduling) {
this.rescheduling = rescheduling;
return this;
}
/**
* Sets the executor service to which the tasks are submitted once the task's delay is up.
*
* @param executorService The executor service for running the task
* @return A reference to this builder for chaining
*/
public Builder withExecutor(final ExecutorService executorService) {
this.executorService = executorService;
return this;
}
/**
* Sets the task for a one-shot schedule (i.e. is only executed once after the delay)
*
* @param task The task to be executed.
* @return A reference to this builder for chaining
*/
public Builder withOneShot(final Supplier task) {
this.task = task;
this.scheduleType = ScheduleType.ONE_SHOT;
return this;
}
/**
* Sets the task for a periodic schedule (i.e. executed periodically after the initial delay). The task
* is submitted for execution after a delay after previous the task has completed.
*
* @param task The task to be executed.
* @param scheduleType The type of schedule ({@link ScheduleType#FIXED_DELAY} or {@link ScheduleType#FIXED_RATE})
* @param timesAround The number of times around the timer the cursor needs to move before executing the
* period task, after the initial delay.
* @param wheelOffset The offset in the wheel, from the current cursor, for the bucket that holds the
* periodic task, after the initial delay.
* @param timeout The time-out for waiting for the task to finish executing
* @return A reference to this builder for chaining
*/
public Builder withPeriodic(final Supplier task,
final ScheduleType scheduleType,
final int timesAround,
final int wheelOffset,
final Duration timeout) {
this.task = task;
this.scheduleType = scheduleType;
this.periodicWheelOffset = wheelOffset;
this.periodicTimesAround = timesAround;
this.timeout = timeout;
return this;
}
/**
* @return A validated {@link ScheduledTask} instance
*/
public ScheduledTask build() {
if(Objects.isNull(timesAround) || timesAround < 0) {
final String message = "Must specify the number of times around the wheel and that value must be non-negative";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(Objects.isNull(wheelOffset) || wheelOffset < 0) {
final String message = "Must specify the wheel offset and that value must be non-negative";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(Objects.isNull(task)) {
final String message = "Must specify the scheduled task";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(Objects.isNull(executorService)) {
final String message = "Must specify the executor service for task processing";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(scheduleType == FIXED_RATE || scheduleType == FIXED_DELAY) {
if(Objects.isNull(periodicTimesAround) || periodicTimesAround < 0) {
final String message = "Must specify the number of times around the wheel for periodic schedule and that value must be non-negative";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(Objects.isNull(periodicWheelOffset) || periodicWheelOffset < 0) {
final String message = "Must specify the wheel offset for periodic schedule and that value must be non-negative";
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(scheduleType == FIXED_DELAY && (Objects.isNull(timeout) || timeout.isNegative() || timeout.isZero())) {
final String message = String.format(
"Fixed delay schedules require a task time-out that is a positive value; timeout: %,d µs",
timeout.toNanos() * 1000
);
LOGGER.error(message);
throw new IllegalStateException(message);
}
if(Objects.isNull(rescheduling)) {
final String message = "Must specify the rescheduling callback";
LOGGER.error(message);
throw new IllegalStateException(message);
}
} else {
periodicWheelOffset = -1;
periodicTimesAround = -1;
}
return new ScheduledTask<>(
timesAround, wheelOffset,
periodicTimesAround, periodicWheelOffset,
timeout, task, scheduleType, rescheduling, executorService
);
}
}
}