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

com.fluxtion.agrona.DeadlineTimerWheel Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014-2024 Real Logic Limited.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.fluxtion.agrona;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * Timer Wheel for timers scheduled to expire on a deadline, (NOT thread safe).
 * 

* Based on netty's HashedTimerWheel, which is based on * George Varghese and * Tony Lauck's paper, * 'Hashed * and Hierarchical Timing Wheels: data structures to efficiently implement a * timer facility'. More comprehensive slides are located * here. *

* Wheel is backed by arrays. Timer cancellation is O(1). Timer scheduling might be slightly * longer if a lot of timers are in the same tick or spoke. The underlying tick is contained in an array. That * array grows when needed, but does not shrink. *

* Caveats *

* Timers that expire in the same tick are not ordered with one another. As ticks are * fairly coarse resolution normally, this means that some timers may expire out of order. *

* Note: Not threadsafe. */ public class DeadlineTimerWheel { /** * Represents a deadline not set in the wheel. */ public static final long NULL_DEADLINE = Long.MAX_VALUE; private static final int INITIAL_TICK_ALLOCATION = 16; private final long tickResolution; private long startTime; private long currentTick; private long timerCount; private final int ticksPerWheel; private final int tickMask; private final int resolutionBitsToShift; private int tickAllocation; private int allocationBitsToShift; private int pollIndex; private final TimeUnit timeUnit; private long[] wheel; /** * Handler for processing expired timers. * * @see DeadlineTimerWheel#poll(long, TimerHandler, int) */ @FunctionalInterface public interface TimerHandler { /** * Called when the deadline has expired. * * @param timeUnit for the time. * @param now for the expired timer. * @param timerId for the expired timer. * @return true to consume the timer, or false to keep timer active and abort further polling. */ boolean onTimerExpiry(TimeUnit timeUnit, long now, long timerId); } /** * Consumer of timer entries as deadline to timerId. * * @see DeadlineTimerWheel#forEach(TimerConsumer) */ @FunctionalInterface public interface TimerConsumer { /** * Called for each timer in the Timer Wheel. * * @param deadline of the timer. * @param timerId of the timer. */ void accept(long deadline, long timerId); } /** * Construct timer wheel and configure timing with default initial allocation. * * @param timeUnit for the values used to express the time. * @param startTime for the wheel (in given {@link TimeUnit}). * @param tickResolution for the wheel, i.e. how many {@link TimeUnit}s per tick. * @param ticksPerWheel or spokes, for the wheel (must be power of 2). */ public DeadlineTimerWheel( final TimeUnit timeUnit, final long startTime, final long tickResolution, final int ticksPerWheel) { this(timeUnit, startTime, tickResolution, ticksPerWheel, INITIAL_TICK_ALLOCATION); } /** * Construct timer wheel and configure timing with provided initial allocation. * * @param timeUnit for the values used to express the time. * @param startTime for the wheel (in given {@link TimeUnit}). * @param tickResolution for the wheel, i.e. how many {@link TimeUnit}s per tick. * @param ticksPerWheel or spokes, for the wheel (must be power of 2). * @param initialTickAllocation space allocated per tick of the wheel (must be power of 2). */ public DeadlineTimerWheel( final TimeUnit timeUnit, final long startTime, final long tickResolution, final int ticksPerWheel, final int initialTickAllocation) { checkTicksPerWheel(ticksPerWheel); checkResolution(tickResolution); checkInitialTickAllocation(initialTickAllocation); this.timeUnit = timeUnit; this.ticksPerWheel = ticksPerWheel; this.tickAllocation = initialTickAllocation; this.tickMask = ticksPerWheel - 1; this.tickResolution = tickResolution; this.resolutionBitsToShift = Long.numberOfTrailingZeros(tickResolution); this.allocationBitsToShift = Integer.numberOfTrailingZeros(initialTickAllocation); this.startTime = startTime; wheel = new long[ticksPerWheel * initialTickAllocation]; Arrays.fill(wheel, NULL_DEADLINE); } /** * Time unit for the time ticks. * * @return time unit for the ticks. */ public TimeUnit timeUnit() { return timeUnit; } /** * Resolution of a tick of the wheel in {@link #timeUnit()}s. * * @return resolution of a tick of the wheel in {@link #timeUnit()}s. */ public long tickResolution() { return tickResolution; } /** * The number of ticks, or spokes, per wheel. * * @return number of ticks, or spokes, per wheel. */ public int ticksPerWheel() { return ticksPerWheel; } /** * The start time tick for the wheel from which it advances. * * @return start time tick for the wheel from which it advances. */ public long startTime() { return startTime; } /** * Number of active timers. * * @return number of currently scheduled timers. */ public long timerCount() { return timerCount; } /** * Reset the start time of the wheel. * * @param startTime to set the wheel to. * @throws IllegalStateException if wheel has any scheduled timers. */ public void resetStartTime(final long startTime) { if (timerCount > 0) { throw new IllegalStateException("can not reset startTime with active timers"); } this.startTime = startTime; this.currentTick = 0; this.pollIndex = 0; } /** * Time of current tick of the wheel in {@link #timeUnit()}s. * * @return time of the current tick of the wheel in {@link #timeUnit()}s. */ public long currentTickTime() { return currentTickTime0(); } /** * Set the current tick of the wheel to examine on the next {@link #poll}. *

* If the time passed in is less than the current time, nothing is changed. * No timers will be expired when winding forward and thus are still in the wheel and will be expired as * encountered in the wheel during {@link #poll} operations. No guarantee of order for expired timers is * assumed when later polled. * * @param now current time to advance to or stay at current time. */ public void currentTickTime(final long now) { currentTick = Math.max((now - startTime) >> resolutionBitsToShift, currentTick); } /** * Clear out all scheduled timers in the wheel. */ public void clear() { long remainingTimers = timerCount; if (0 == remainingTimers) { return; } for (int i = 0, length = wheel.length; i < length; i++) { if (NULL_DEADLINE != wheel[i]) { wheel[i] = NULL_DEADLINE; if (--remainingTimers <= 0) { timerCount = 0; return; } } } } /** * Schedule a timer for a given absolute time as a deadline in {@link #timeUnit()}s. A timerId will be assigned * and returned for future reference. * * @param deadline after which the timer should expire. * @return timerId assigned for the scheduled timer. */ public long scheduleTimer(final long deadline) { final long deadlineTick = Math.max((deadline - startTime) >> resolutionBitsToShift, currentTick); final int spokeIndex = (int)(deadlineTick & tickMask); final int tickStartIndex = spokeIndex << allocationBitsToShift; for (int i = 0; i < tickAllocation; i++) { final int index = tickStartIndex + i; if (NULL_DEADLINE == wheel[index]) { wheel[index] = deadline; timerCount++; return timerIdForSlot(spokeIndex, i); } } return increaseCapacity(deadline, spokeIndex); } /** * Cancel a previously scheduled timer. * * @param timerId of the timer to cancel. * @return true if successful otherwise false if the timerId did not exist. */ public boolean cancelTimer(final long timerId) { final int spokeIndex = tickForTimerId(timerId); final int tickIndex = indexInTickArray(timerId); final int wheelIndex = (spokeIndex << allocationBitsToShift) + tickIndex; if (spokeIndex < ticksPerWheel) { if (tickIndex < tickAllocation && NULL_DEADLINE != wheel[wheelIndex]) { wheel[wheelIndex] = NULL_DEADLINE; timerCount--; return true; } } return false; } /** * Poll for timers expired by the deadline passing. * * @param now current time to compare deadlines against. * @param handler to call for each expired timer. * @param expiryLimit to process in one poll operation. * @return count of expired timers as a result of this poll operation. */ public int poll(final long now, final TimerHandler handler, final int expiryLimit) { int timersExpired = 0; if (timerCount > 0) { final int spokeIndex = (int)currentTick & tickMask; for (int i = 0, length = tickAllocation; i < length && expiryLimit > timersExpired; i++) { final int wheelIndex = (spokeIndex << allocationBitsToShift) + pollIndex; final long deadline = wheel[wheelIndex]; if (now >= deadline) { wheel[wheelIndex] = NULL_DEADLINE; timerCount--; timersExpired++; if (!handler.onTimerExpiry(timeUnit, now, timerIdForSlot(spokeIndex, pollIndex))) { wheel[wheelIndex] = deadline; timerCount++; return --timersExpired; } } pollIndex = (pollIndex + 1) >= length ? 0 : (pollIndex + 1); } if (expiryLimit > timersExpired && now >= currentTickTime0()) { currentTick++; pollIndex = 0; } else if (pollIndex >= tickAllocation) { pollIndex = 0; } } else if (now >= currentTickTime0()) { currentTick++; pollIndex = 0; } return timersExpired; } /** * Iterate over wheel so all active timers can be consumed without expiring them. * * @param consumer to call for each active timer. */ public void forEach(final TimerConsumer consumer) { long timersRemaining = timerCount; for (int i = 0, length = wheel.length; i < length; i++) { final long deadline = wheel[i]; if (NULL_DEADLINE != deadline) { consumer.accept(deadline, timerIdForSlot(i >> allocationBitsToShift, i & tickMask)); if (--timersRemaining <= 0) { return; } } } } /** * Get the deadline for the given timerId. * * @param timerId of the timer to return the deadline of. * @return deadline for the given timerId or {@link #NULL_DEADLINE} if timerId is not scheduled. */ public long deadline(final long timerId) { final int spokeIndex = tickForTimerId(timerId); final int tickIndex = indexInTickArray(timerId); final int wheelIndex = (spokeIndex << allocationBitsToShift) + tickIndex; if (spokeIndex < ticksPerWheel) { if (tickIndex < tickAllocation) { return wheel[wheelIndex]; } } return NULL_DEADLINE; } private long currentTickTime0() { return ((currentTick + 1L) << resolutionBitsToShift) + startTime; } private long increaseCapacity(final long deadline, final int spokeIndex) { final int newTickAllocation = tickAllocation << 1; final int newAllocationBitsToShift = Integer.numberOfTrailingZeros(newTickAllocation); final long newCapacity = (long)ticksPerWheel * newTickAllocation; if (newCapacity > (1 << 30)) { throw new IllegalStateException("max capacity reached at tickAllocation=" + tickAllocation); } final long[] newWheel = new long[(int)newCapacity]; Arrays.fill(newWheel, NULL_DEADLINE); for (int j = 0; j < ticksPerWheel; j++) { final int oldTickStartIndex = j << allocationBitsToShift; final int newTickStartIndex = j << newAllocationBitsToShift; System.arraycopy(wheel, oldTickStartIndex, newWheel, newTickStartIndex, tickAllocation); } newWheel[(spokeIndex << newAllocationBitsToShift) + tickAllocation] = deadline; final long timerId = timerIdForSlot(spokeIndex, tickAllocation); timerCount++; tickAllocation = newTickAllocation; allocationBitsToShift = newAllocationBitsToShift; wheel = newWheel; return timerId; } private static long timerIdForSlot(final int tickOnWheel, final int tickArrayIndex) { return ((long)tickOnWheel << 32) | tickArrayIndex; } private static int tickForTimerId(final long timerId) { return (int)(timerId >> 32); } private static int indexInTickArray(final long timerId) { return (int)timerId; } private static void checkTicksPerWheel(final int ticksPerWheel) { if (!BitUtil.isPowerOfTwo(ticksPerWheel)) { throw new IllegalArgumentException("ticks per wheel must be a power of 2: " + ticksPerWheel); } } private static void checkResolution(final long tickResolution) { if (!BitUtil.isPowerOfTwo(tickResolution)) { throw new IllegalArgumentException("tick resolution must be a power of 2: " + tickResolution); } } private static void checkInitialTickAllocation(final int tickAllocation) { if (!BitUtil.isPowerOfTwo(tickAllocation)) { throw new IllegalArgumentException("tick allocation must be a power of 2: " + tickAllocation); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy