All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.digitalcipher.spiked.timing.HashedWheelTimer Maven / Gradle / Ivy

package com.digitalcipher.spiked.timing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.IntStream;

/**
 * Original code contained the following header:

* Hash Wheel Timer, as per the paper: *

* Hashed and hierarchical timing wheels: * http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf *

* More comprehensive slides, explaining the paper can be found here: * http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt *

* Hash Wheel timer is an approximated timer that allows performant execution of * larger amount of tasks with better performance compared to traditional scheduling. * * @author Oleksandr Petrov * * Updated hashed-wheel-timer that supports reasonable sub-millisecond delays (with * delays down to about 100 µs with 50 µs resolution). The basic approach is that * of the original code, but much of it has been rewritten in an attempt to get * the sub-millisecond delays with reasonable presicsion and accuracy. * * To construct a hashed-wheel-timer, use the {@link HashedWheelTimer.Builder}. And * once constructed, call {@link HashedWheelTimer#start()} to start the timer * processing loop. Once done with the timer, call the {@link HashedWheelTimer#shutdown()} * or {@link HashedWheelTimer#shutdownNow()} method. * * @author Rob Philipp */ public class HashedWheelTimer { private static final Logger LOGGER = LoggerFactory.getLogger(HashedWheelTimer.class); private final int wheelSize; private final long resolution; private final ExecutorService loop; private final ExecutorService executor; private final WaitStrategy waitStrategy; private final ConcurrentMap>> wheel; private final AtomicInteger cursor = new AtomicInteger(0); private final ReentrantLock lock = new ReentrantLock(); /** * Create a new {@code HashedWheelTimer} using the given timer resolution and wheelSize. All times will * rounded up to the closest multiple of this resolution. * @param name name for daemon thread factory to be displayed * @param resolution resolution of this timer in NANOSECONDS * @param wheelSize size of the Ring Buffer supporting the Timer, the larger the wheel, the less the lookup time is * for sparse timeouts. Sane default is 512. * @param strategy waitStrategy for waiting for the next tick * @param executor Executor instance to submit tasks to once the scheduled time has been hit */ private HashedWheelTimer(final String name, final long resolution, final int wheelSize, final WaitStrategy strategy, final ExecutorService executor) { this.waitStrategy = strategy; this.wheelSize = wheelSize; this.resolution = resolution; this.executor = executor; // create the timer wheel and the set holding the scheduled tasks this.wheel = new ConcurrentHashMap<>(wheelSize); IntStream .range(0, wheelSize) .forEach(bucket -> wheel.putIfAbsent( bucket, new ConcurrentSkipListSet<>(Comparator.comparingInt(ScheduledTask::wheelOffset)) )); // sets the timer-executor service in which the timer's update loop runs this.loop = timerExecutorService(name); } /** * @return Builder for constructing a validated {@link HashedWheelTimer} instance */ public static Builder builder() { return new Builder(); } /** * Creates a single-thread executor for running the loop * * @param timerName The name of the timer * @return a single-thread executor with a factory to name the threads */ private ExecutorService timerExecutorService(final String timerName) { return Executors.newSingleThreadExecutor(new ThreadFactory() { final AtomicInteger numThreads = new AtomicInteger(); /** * Creates a new thread for the runnable * @param runnable The runnable * @return a new thread for the runnable */ @Override public Thread newThread(final Runnable runnable) { final Thread thread = new Thread(runnable, timerName + "-" + numThreads.getAndIncrement()); thread.setDaemon(true); return thread; } }); } /** * Starts the hashed-wheel-timer's timer loop * @return A reference to this instance for chaining */ public HashedWheelTimer start() { loop.submit(timerLoop()); return this; } /** * Attempts to gracefully shut-down the timer loop and any executing processes */ public void shutdown() { loop.shutdown(); executor.shutdown(); } /** * Attempts to shut down the timer loop and any executing processes * @return A list of tasks that were halted as a result of this call. */ public List shutdownNow() { loop.shutdownNow(); return executor.shutdownNow(); } /** * Blocks until all scheduled tasks and timer-loop tasks have completed execution after a shutdown * request, or the timeout occurs, or the current thread is interrupted, whichever happens first. * @param timeout The amount of time to wait * @param unit The unit of time association with the timeout * @return {@code true} if the schedule task executor and the timer-loop executor have terminated; * {@code false} otherwise * @throws InterruptedException If interrupted while waiting */ public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { return loop.awaitTermination(timeout, unit) && executor.awaitTermination(timeout, unit); } /** * @return {@code true} if the timer-loop and scheduled-task executor are shut down; {@code false} otherwise */ public boolean isShutdown() { return loop.isShutdown() && executor.isShutdown(); } /** * @return {@code true} if the timer-loop and scheduled-task executor are terminated; {@code false} otherwise */ public boolean isTerminated() { return loop.isTerminated() && executor.isTerminated(); } /** * Returns a runnable function that updates the cursor, runs processes at their scheduled time, and * calls the wait function before processing the next bucket in the wheel. The loop is exited when the * {@link WaitStrategy#waitUntil(long)} method returns an interrupted flag. * * @return A timer-loop function */ private Runnable timerLoop() { return () -> { long deadline = System.nanoTime(); boolean interrupted; do { // grab and update the current cursor lock.lock(); final int currentCursor = cursor.getAndUpdate(val -> (val + 1) % wheelSize); // update the scheduled tasks wheel.computeIfPresent(wheelIndex(currentCursor), (index, scheduledTasks) -> { // update the registration list based on the results of the call to process the task // the task.process does the following: // 1. executes the task if ready // 2. reschedules the task after processing for periodic tasks, // and upon completion, returns // 1. null if cancelled or after one-shot has executed // 2. the task if rescheduled // All the tasks that are set to null (i.e. cancelled or one-shot) are removed from // the list. scheduledTasks.removeIf(task -> Objects.isNull(task.process())); // < 1 µs with no tasks, approx. 40 to 80 µs with tasks return scheduledTasks; }); lock.unlock(); // updates the deadline for the registrations in the next wheel bucket, and provides a // catch-up in case the registration updates take more than the resolution final long currentTime = System.nanoTime(); deadline = Math.max(deadline + resolution, currentTime); // wait for the deadline if it isn't equal to the current time interrupted = deadline != currentTime && waitStrategy.waitUntil(deadline); } while (!interrupted); }; } /** * Schedule a task to run after the specified delay * @param task The task to run (called when the delay is over) * @param delay The amount of time to delay * @param timeUnit The unit associated with the delay time * @param The return type from the task * @return A future that will hold the task result upon completion */ public CompletableFuture schedule(final Supplier task, final long delay, final TimeUnit timeUnit) { return scheduleOneShot(TimeUnit.NANOSECONDS.convert(delay, timeUnit), task); } /** * Schedules the task at a fixed rate (i.e. doesn't wait for the task to complete). Will attempt to * run the tasks periodically with the specified period. * @param task The task to be executed * @param timeout The duration of the timer * @param timeoutUnits The units of the timer duration * @param initialDelay The initial delay * @param period The periodic delay * @param unit The time unit associated with the delay and the period * @param The return type of the task * @return The future holding for the task */ public CompletableFuture scheduleAtFixedRate(final Supplier task, final long timeout, final TimeUnit timeoutUnits, final long initialDelay, final long period, final TimeUnit unit) { return schedulePeriodic( ScheduleType.FIXED_RATE, TimeUnit.NANOSECONDS.convert(initialDelay, unit), TimeUnit.NANOSECONDS.convert(period, unit), Duration.ofNanos(TimeUnit.NANOSECONDS.convert(timeout, timeoutUnits)), task ); } /** * Schedule a task executed after an initial delay and then repeatedly with a delay of {@code period} * after the tasks has completed execution. * @param task The task to be executed * @param timeout The duration of the timer * @param timeoutUnits The units of the timer duration * @param initialDelay The initial delay * @param period The periodic delay * @param unit The time unit associated with the delay and the period * @param The return type of the task * @return The future holding for the task */ public CompletableFuture scheduleWithFixedDelay(final Supplier task, final long timeout, final TimeUnit timeoutUnits, final long initialDelay, final long period, final TimeUnit unit) { return schedulePeriodic( ScheduleType.FIXED_DELAY, TimeUnit.NANOSECONDS.convert(initialDelay, unit), TimeUnit.NANOSECONDS.convert(period, unit), Duration.ofNanos(TimeUnit.NANOSECONDS.convert(timeout, timeoutUnits)), task ); } // /** // * Create a wrapper Function, which will "debounce" i.e. postpone the function execution until after period // * has elapsed since last time it was invoked. delegate will be called most once period. // * // * @param delegate delegate runnable to be wrapped // * @param period given time period // * @param timeUnit unit of the period // * @return wrapped runnable // */ // public Runnable debounce(Runnable delegate, // long period, // TimeUnit timeUnit) { // AtomicReference> reg = new AtomicReference<>(); // // return () -> { // ScheduledFuture future = reg.getAndSet(scheduleOneShot(TimeUnit.NANOSECONDS.convert(period, timeUnit), // () -> { // delegate.run(); // return null; // })); // if (future != null) { // future.cancel(true); // } // }; // } // // /** // * Create a wrapper Consumer, which will "debounce" i.e. postpone the function execution until after period // * has elapsed since last time it was invoked. delegate will be called most once period. // * // * @param delegate delegate consumer to be wrapped // * @param period given time period // * @param timeUnit unit of the period // * @return wrapped runnable // */ // public Consumer debounce(Consumer delegate, // long period, // TimeUnit timeUnit) { // AtomicReference> reg = new AtomicReference<>(); // // return t -> { // ScheduledFuture future = reg.getAndSet(scheduleOneShot(TimeUnit.NANOSECONDS.convert(period, timeUnit), // () -> { // delegate.accept(t); // return t; // })); // if (future != null) { // future.cancel(true); // } // }; // } // // /** // * Create a wrapper Runnable, which creates a throttled version, which, when called repeatedly, will call the // * original function only once per every period milliseconds. It's easier to think about throttle // * in terms of it's "left bound" (first time it's called within the current period). // * // * @param delegate delegate runnable to be called // * @param period period to be elapsed between the runs // * @param timeUnit unit of the period // * @return wrapped runnable // */ // public Runnable throttle(final Runnable delegate, final long period, final TimeUnit timeUnit) { // final AtomicBoolean alreadyWaiting = new AtomicBoolean(); // // return () -> { // if (alreadyWaiting.compareAndSet(false, true)) { // scheduleOneShot(TimeUnit.NANOSECONDS.convert(period, timeUnit), // () -> { // delegate.run(); // alreadyWaiting.compareAndSet(true, false); // return null; // }); // } // }; // } // // /** // * Create a wrapper Consumer, which creates a throttled version, which, when called repeatedly, will call the // * original function only once per every period milliseconds. It's easier to think about throttle // * in terms of it's "left bound" (first time it's called within the current period). // * // * @param delegate delegate consumer to be called // * @param period period to be elapsed between the runs // * @param timeUnit unit of the period // * @return wrapped runnable // */ // public Consumer throttle(final Consumer delegate, final long period, final TimeUnit timeUnit) { // final AtomicBoolean alreadyWaiting = new AtomicBoolean(); // final AtomicReference lastValue = new AtomicReference<>(); // // return val -> { // lastValue.set(val); // if (alreadyWaiting.compareAndSet(false, true)) { // scheduleOneShot(TimeUnit.NANOSECONDS.convert(period, timeUnit), // () -> { // delegate.accept(lastValue.getAndSet(null)); // alreadyWaiting.compareAndSet(true, false); // return null; // }); // } // }; // } /** * {@inheritDoc} */ @Override public String toString() { return String.format("HashedWheelTimer { Buffer Size: %d, Resolution: %d }", wheelSize, resolution); } /** * Calculates the index in the wheel given the cursor and the wheel size * @param cursor The current cursor position * @return The index into the wheel (accounting for the fact that the cursor may * be larger than the wheel size). */ private int wheelIndex(final int cursor) { return cursor % wheelSize; } /** * Calculates the bucket in the wheel that the specified delay duration falls * @param duration The duration in nanoseconds * @return The index of the bucket into which the duration falls */ private int wheelOffset(final long duration) { return (int) Math.max(0, (duration / resolution)); } /** * Calculates the number of times around the wheel a scheduled delay represents. For example, * suppose that the resolution is 1 ms and there are 32 buckets. Then once around the wheel * represents a wait of 32 ms. A delay of 67 ms would have an offset of 67, which would be in * the third bucket after the cursor moved around the bucket twice. * @param wheelOffset The wheel offset given by the {@link #wheelOffset(long)} method * @return The number of times-around the wheel for the specified offset. */ private int wheelPeriods(final int wheelOffset) { return wheelOffset / wheelSize; } /** * Asserts that the timer loop is running and that the first delay is not smaller than the * resolution. * @param firstDelay The first delay. */ private void assertRunningAndValid(final long firstDelay) { // ensure that the timer is still running and that the delay is greater or equal to the resulotion if (loop.isTerminated()) { final String message = "Cannot schedule tasks when the hash-wheel-timer is not longer running."; LOGGER.error(message); throw new IllegalStateException(message); } if (firstDelay < resolution) { final String message = String.format( "Schedule delay must be greater than or equal to the timer resolution; delay: %,d ns; resolution: %,d ns.", firstDelay, resolution ); LOGGER.error(message); throw new IllegalArgumentException(message); } } /** * Schedules a one-shot delay * * @param firstDelay The delay in nanoseconds * @param task The supplier that is run when the delay expires * @param The return type of the task * @return The completion future that is completed once the task is run */ private CompletableFuture scheduleOneShot(final long firstDelay, final Supplier task) { // grab the start time so that we can correct the delay for the amount of time it took to // instantiate the scheduled tasks final long start = System.nanoTime(); // ensure that the timer is still running and that the delay is greater or equal to the resolution assertRunningAndValid(firstDelay); // the number of buckets required to cover the delay final int firstFireOffset = wheelOffset(firstDelay); // the number of times around the wheel before the delay is over final int firstFireRounds = wheelPeriods(firstFireOffset); // create the new tasks, and also get a rough estimate of the time it took to instantiate the task final ScheduledTask scheduledTask = ScheduledTask.builder() .withInitialDelayInfo(firstFireRounds, firstFireOffset) .withExecutor(executor) .withOneShot(task) .build(); final long instantiationTime = System.nanoTime() - start; if (firstDelay - instantiationTime <= 0) { System.out.println(String.format("Completed immediately; instantiation time: %,d ns", instantiationTime)); return scheduledTask.executeNow(); } // calculate the wheel offset after subtraction the instantiation time from the request delay final int adjustedOffset = wheelOffset(firstDelay - instantiationTime); // add the registration to the bucket the will be processed next. the cursor is always set to the // bucket the will be processed next. lock.lock(); wheel.computeIfPresent(wheelIndex(cursor.get() + adjustedOffset), (index, registrations) -> { registrations.add(scheduledTask); return registrations; }); lock.unlock(); return scheduledTask; } /** * Schedules the specified task to be executed after an initial delay, then periodically, after the * task has completed, at specified periodic delay. * @param firstDelay The first delay (in nanoseconds) after which to execute the task * @param periodicDelay The periodic delay (in nanoseconds) after the task has completed, to execute the task * @param timeout The duration of the periodic timer (time until it stops) * @param task The task to execute * @param The return type of the task * @return The completable future with the result. */ private CompletableFuture schedulePeriodic(final ScheduleType scheduleType, final long firstDelay, final long periodicDelay, final Duration timeout, final Supplier task) { // grab the start time so that we can correct the delay for the amount of time it took to // instantiate the scheduled tasks final long start = System.nanoTime(); // ensure that the timer is still running and that the delay is greater or equal to the resolution assertRunningAndValid(firstDelay); // the number of buckets required to cover the delay final int firstFireOffset = wheelOffset(firstDelay); final int periodicFireOffset = wheelOffset(periodicDelay); // the number of times around the wheel before the delay is over final int firstFireRounds = wheelPeriods(firstFireOffset); final int periodicTimeAround = wheelPeriods(periodicFireOffset); // create the new tasks, and also get a rough estimate of the time it took to instantiate the task final ScheduledTask scheduledTask = ScheduledTask.builder() .withInitialDelayInfo(firstFireRounds, firstFireOffset) .withRescheduling(this::reschedule) .withExecutor(executor) .withPeriodic(task, scheduleType, periodicTimeAround, periodicFireOffset, timeout) .build(); final long instantiationTime = System.nanoTime() - start; if (firstDelay - instantiationTime <= 0) { return scheduledTask.executeNow(); } // calculate the wheel offset after subtraction the instantiation time from the request delay final int adjustedOffset = wheelOffset(firstDelay - instantiationTime); // add the registration to the bucket the will be processed next. the cursor is always set to the // bucket the will be processed next. note that when the wheel is set up, a concurrent skip list is // added to each bucket, and so the an index will always be present lock.lock(); wheel.computeIfPresent( wheelIndex(cursor.get() + adjustedOffset), (index, registrations) -> { registrations.add(scheduledTask); return registrations; }); lock.unlock(); return scheduledTask; } /** * Reschedule a periodic task for that has completed to execute the tasks at the next * delay. * * @param registration The registration to reschedule */ private void reschedule(final ScheduledTask registration, final long start) { lock.lock(); final long instantiationTime = System.nanoTime() - start; final int adjustedOffset = Math.max(0, registration.periodicWheelOffset() - wheelOffset(instantiationTime)); wheel.get(wheelIndex(cursor.get() + adjustedOffset)).add(registration); lock.unlock(); } /** * Builder for constructing validated {@link HashedWheelTimer} instances */ @SuppressWarnings({"unused", "WeakerAccess"}) public static class Builder { private static final String DEFAULT_TIMER_NAME = "hashed-wheel-timer"; private static final long DEFAULT_RESOLUTION = 10; private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS; private static final int DEFAULT_WHEEL_SIZE = 512; private String timerName; private Long resolution; private TimeUnit units; private Integer wheelSize; private WaitStrategy waitStrategy; private ExecutorService executor; /** * @param name name for daemon thread factory to be displayed * @return a reference to this builder for chaining */ public Builder withTimerName(final String name) { this.timerName = name; return this; } /** * Sets the default name ({@value DEFAULT_TIMER_NAME}) for the timer's thread factory * @return a reference to this builder for chaining */ public Builder withDefaultTimerName() { this.timerName = DEFAULT_TIMER_NAME; return this; } /** * Sets the time-resolution for the timer. Generally, this is the most precise the timer will be. * For example, setting the resolution to 1 and the time units to milliseconds, means that the * timer, in best case, is precise to 1 ms. * @param resolution resolution of this timer * @param units The time-units for the resolution * @return a reference to this builder for chaining */ public Builder withResolution(final long resolution, final TimeUnit units) { this.resolution = resolution; this.units = units; return this; } /** * Sets the default resolution ({@value Builder#DEFAULT_RESOLUTION} milliseconds) for the timer * @return a reference to this builder for chaining */ public Builder withDefaultResolution() { this.resolution = DEFAULT_RESOLUTION; this.units = DEFAULT_TIME_UNIT; return this; } /** * Sets the number of buckets in the hash-wheel. * @param numBuckets The number of buckets in the hash-wheel * @return a reference to this builder for chaining */ public Builder withWheelSize(final int numBuckets) { this.wheelSize = numBuckets; return this; } /** * Sets the default wheel size ({@value DEFAULT_WHEEL_SIZE}) for the timer * @return a reference to this builder for chaining */ public Builder withDefaultWheelSize() { this.wheelSize = DEFAULT_WHEEL_SIZE; return this; } /** * Sets how wait-time between successive buckets is implemented. For example, * {@link WaitStrategy.BusySpinWait} loops through successive calls to {@link System#nanoTime()} * until the deadline has been reached to move to the next bucket. * @param strategy The wait strategy * @return a reference to this builder for chaining */ public Builder withWaitStrategy(final WaitStrategy strategy) { this.waitStrategy = strategy; return this; } /** * Sets the executor that is used to process the time loop. * @param executor The timer-loop executor * @return a reference to this builder for chaining */ public Builder withExecutor(final ExecutorService executor) { this.executor = executor; return this; } /** * Sets the default executor ({@link Executors#newFixedThreadPool(int)}) with one thread. * @return a reference to this builder for chaining */ public Builder withDefaultExecutor() { this.executor = Executors.newFixedThreadPool(1); return this; } /** * Sets the default executor ({@link Executors#newFixedThreadPool(int)}) with one thread. * @param numThreads The number of threads for the executor * @return a reference to this builder for chaining */ public Builder withDefaultExecutor(final int numThreads) { this.executor = Executors.newFixedThreadPool(numThreads); return this; } /** * @return A validated {@link HashedWheelTimer} instance */ public HashedWheelTimer build() { if (Objects.isNull(timerName) || timerName.isEmpty()) { final String message = "Timer name must be specified and cannot be empty"; LOGGER.error(message); throw new IllegalStateException(message); } if (Objects.isNull(resolution) || resolution <= 0) { final String message = "Timer resolution must be specified and must be a positive number"; LOGGER.error(message); throw new IllegalStateException(message); } if (Objects.isNull(units)) { final String message = "Units for the timer resolution must be specified and cannot be null"; LOGGER.error(message); throw new IllegalStateException(message); } if (TimeUnit.MICROSECONDS.convert(resolution, units) < 50) { final String message = String.format("Timer resolution must be 50 µs or greater; specified: %d %s", resolution, units.toString()); LOGGER.error(message); throw new IllegalStateException(message); } if (Objects.isNull(waitStrategy)) { final String message = "Timer wait strategy must be specified and cannot be null"; LOGGER.error(message); throw new IllegalStateException(message); } if (Objects.isNull(executor)) { final String message = "Executor for the timer loop must be specified and cannot be null; or use withDefaultExecutor() method."; LOGGER.error(message); throw new IllegalStateException(message); } final long resolutionNanos = TimeUnit.NANOSECONDS.convert(resolution, units); return new HashedWheelTimer(timerName, resolutionNanos, wheelSize, waitStrategy, executor); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy