/* * Copyright 2017 Ben Manes. All Rights Reserved. * * 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 * * http://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.github.benmanes.caffeine.cache; import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument; import static java.util.Objects.requireNonNull; import java.lang.ref.ReferenceQueue; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.function.Function; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; /** * A hierarchical timer wheel to add, remove, and fire expiration events in amortized O(1) time. The * expiration events are deferred until the timer is advanced, which is performed as part of the * cache's maintenance cycle. * * @author ben.manes@gmail.com (Ben Manes) */ @NotThreadSafe final class TimerWheel<K, V> { /* * A timer wheel [1] stores timer events in buckets on a circular buffer. A bucket represents a * coarse time span, e.g. one minute, and holds a doubly-linked list of events. The wheels are * structured in a hierarchy (seconds, minutes, hours, days) so that events scheduled in the * distant future are cascaded to lower buckets when the wheels rotate. This allows for events * to be added, removed, and expired in O(1) time, where expiration occurs for the entire bucket, * and penalty of cascading is amortized by the rotations. * * [1] Hashed and Hierarchical Timing Wheels * http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf */ static final int[] BUCKETS = { 64, 64, 32, 4, 1 }; static final long[] SPANS = { ceilingPowerOfTwo(TimeUnit.SECONDS.toNanos(1)), // 1.07s ceilingPowerOfTwo(TimeUnit.MINUTES.toNanos(1)), // 1.14m ceilingPowerOfTwo(TimeUnit.HOURS.toNanos(1)), // 1.22h ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 1.63d BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5d BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5d }; static final long[] SHIFT = { Long.SIZE - Long.numberOfLeadingZeros(SPANS[0] - 1), Long.SIZE - Long.numberOfLeadingZeros(SPANS[1] - 1), Long.SIZE - Long.numberOfLeadingZeros(SPANS[2] - 1), Long.SIZE - Long.numberOfLeadingZeros(SPANS[3] - 1), Long.SIZE - Long.numberOfLeadingZeros(SPANS[4] - 1), }; final BoundedLocalCache<K, V> cache; final Node<K, V>[][] wheel; long nanos; @SuppressWarnings({"rawtypes", "unchecked"}) TimerWheel(BoundedLocalCache<K, V> cache) { this.cache = requireNonNull(cache); wheel = new Node[BUCKETS.length][1]; for (int i = 0; i < wheel.length; i++) { wheel[i] = new Node[BUCKETS[i]]; for (int j = 0; j < wheel[i].length; j++) { wheel[i][j] = new Sentinel<>(); } } } /** * Advances the timer and evicts entries that have expired. * * @param currentTimeNanos the current time, in nanoseconds */ public void advance(long currentTimeNanos) { long previousTimeNanos = nanos; try { nanos = currentTimeNanos; for (int i = 0; i < SHIFT.length; i++) { long previousTicks = (previousTimeNanos >> SHIFT[i]); long currentTicks = (currentTimeNanos >> SHIFT[i]); if ((currentTicks - previousTicks) <= 0) { break; } expire(i, previousTicks, currentTicks, previousTimeNanos, currentTimeNanos); } } catch (Throwable t) { nanos = previousTimeNanos; throw t; } } /** * Expires entries or reschedules into the proper bucket if still active. * * @param index the wheel being operated on * @param previousTicks the previous number of ticks * @param currentTicks the current number of ticks * @param previousTimeNanos the previous time, in nanoseconds * @param currentTimeNanos the current time, in nanoseconds */ void expire(int index, long previousTicks, long currentTicks, long previousTimeNanos, long currentTimeNanos) { Node<K, V>[] timerWheel = wheel[index]; int start, end; if ((currentTimeNanos - previousTimeNanos) >= SPANS[index + 1]) { end = timerWheel.length; start = 0; } else { long mask = SPANS[index] - 1; start = (int) (previousTicks & mask); end = 1 + (int) (currentTicks & mask); } int mask = timerWheel.length - 1; for (int i = start; i < end; i++) { Node<K, V> sentinel = timerWheel[(i & mask)]; Node<K, V> prev = sentinel.getPreviousInVariableOrder(); Node<K, V> node = sentinel.getNextInVariableOrder(); sentinel.setPreviousInVariableOrder(sentinel); sentinel.setNextInVariableOrder(sentinel); while (node != sentinel) { Node<K, V> next = node.getNextInVariableOrder(); node.setPreviousInVariableOrder(null); node.setNextInVariableOrder(null); try { if (((node.getVariableTime() - currentTimeNanos) > 0) || !cache.evictEntry(node, RemovalCause.EXPIRED, nanos)) { Node<K, V> newSentinel = findBucket(node.getVariableTime()); link(newSentinel, node); } node = next; } catch (Throwable t) { node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); node.setNextInVariableOrder(next); sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); sentinel.setPreviousInVariableOrder(prev); throw t; } } } } /** * Schedules a timer event for the node. * * @param node the entry in the cache */ public void schedule(Node<K, V> node) { Node<K, V> sentinel = findBucket(node.getVariableTime()); link(sentinel, node); } /** * Reschedules an active timer event for the node. * * @param node the entry in the cache */ public void reschedule(Node<K, V> node) { if (node.getNextInVariableOrder() != null) { unlink(node); schedule(node); } } /** * Removes a timer event for this entry if present. * * @param node the entry in the cache */ public void deschedule(Node<K, V> node) { unlink(node); node.setNextInVariableOrder(null); node.setPreviousInVariableOrder(null); } /** * Determines the bucket that the timer event should be added to. * * @param time the time when the event fires * @return the sentinel at the head of the bucket */ Node<K, V> findBucket(long time) { long duration = time - nanos; int length = wheel.length - 1; for (int i = 0; i < length; i++) { if (duration < SPANS[i + 1]) { int ticks = (int) (time >> SHIFT[i]); int index = ticks & (wheel[i].length - 1); return wheel[i][index]; } } return wheel[length][0]; } /** Adds the entry at the tail of the bucket's list. */ void link(Node<K, V> sentinel, Node<K, V> node) { node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder()); node.setNextInVariableOrder(sentinel); sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node); sentinel.setPreviousInVariableOrder(node); } /** Removes the entry from its bucket, if scheduled. */ void unlink(Node<K, V> node) { Node<K, V> next = node.getNextInVariableOrder(); if (next != null) { Node<K, V> prev = node.getPreviousInVariableOrder(); next.setPreviousInVariableOrder(prev); prev.setNextInVariableOrder(next); } } /** * Returns an unmodifiable snapshot map roughly ordered by the expiration time. The wheels are * evaluated in order, but the timers that fall within the bucket's range are not sorted. Beware * that obtaining the mappings is <em>NOT</em> a constant-time operation. * * @param ascending the direction * @param limit the maximum number of entries * @param transformer a function that unwraps the value * @return an unmodifiable snapshot in the desired order */ public Map<K, V> snapshot(boolean ascending, int limit, Function<V, V> transformer) { requireArgument(limit >= 0); Map<K, V> map = new LinkedHashMap<>(Math.min(limit, cache.size())); int startLevel = ascending ? 0 : wheel.length - 1; for (int i = 0; i < wheel.length; i++) { int indexOffset = ascending ? i : -i; int index = startLevel + indexOffset; int ticks = (int) (nanos >> SHIFT[index]); int bucketMask = (wheel[index].length - 1); int startBucket = (ticks & bucketMask) + (ascending ? 1 : 0); for (int j = 0; j < wheel[index].length; j++) { int bucketOffset = ascending ? j : -j; Node<K, V> sentinel = wheel[index][(startBucket + bucketOffset) & bucketMask]; for (Node<K, V> node = traverse(ascending, sentinel); node != sentinel; node = traverse(ascending, node)) { if (map.size() >= limit) { break; } K key = node.getKey(); V value = transformer.apply(node.getValue()); if ((key != null) && (value != null) && node.isAlive()) { map.put(key, value); } } } } return Collections.unmodifiableMap(map); } static <K, V> Node<K, V> traverse(boolean ascending, Node<K, V> node) { return ascending ? node.getNextInVariableOrder() : node.getPreviousInVariableOrder(); } @Override public String toString() { StringBuilder builder = new StringBuilder(); for (int i = 0; i < wheel.length; i++) { Map<Integer, List<K>> buckets = new TreeMap<>(); for (int j = 0; j < wheel[i].length; j++) { List<K> events = new ArrayList<>(); for (Node<K, V> node = wheel[i][j].getNextInVariableOrder(); node != wheel[i][j]; node = node.getNextInVariableOrder()) { events.add(node.getKey()); } if (!events.isEmpty()) { buckets.put(j, events); } } builder.append("Wheel #").append(i + 1).append(": ").append(buckets).append('\n'); } return builder.deleteCharAt(builder.length() - 1).toString(); } /** A sentinel for the doubly-linked list in the bucket. */ static final class Sentinel<K, V> implements Node<K, V> { Node<K, V> prev; Node<K, V> next; Sentinel() { prev = next = this; } @Override public Node<K, V> getPreviousInVariableOrder() { return prev; } @Override public void setPreviousInVariableOrder(@Nullable Node<K, V> prev) { this.prev = prev; } @Override public Node<K, V> getNextInVariableOrder() { return next; } @Override public void setNextInVariableOrder(@Nullable Node<K, V> next) { this.next = next; } @Override public K getKey() { return null; } @Override public Object getKeyReference() { throw new UnsupportedOperationException(); } @Override public V getValue() { return null; } @Override public Object getValueReference() { throw new UnsupportedOperationException(); } @Override public void setValue(V value, ReferenceQueue<V> referenceQueue) {} @Override public boolean containsValue(Object value) { return false; } @Override public boolean isAlive() { return false; } @Override public boolean isRetired() { return false; } @Override public boolean isDead() { return false; } @Override public void retire() {} @Override public void die() {} } static long ceilingPowerOfTwo(long x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1L << -Long.numberOfLeadingZeros(x - 1); } }