Skip to content

Commit c5d75db

Browse files
committed
Per-entry expiration
Add `TimerWheel` to manage the per-entry expiration with amortized O(1) time.
1 parent 791fac9 commit c5d75db

File tree

4 files changed

+239
-0
lines changed

4 files changed

+239
-0
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
"deqs",
2020
"Deque",
2121
"Deques",
22+
"deschedule",
2223
"devcontainer",
2324
"docsrs",
2425
"Einziger",
2526
"else's",
27+
"ENHANCEME",
2628
"Eytan",
2729
"getrandom",
2830
"hashbrown",

src/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) mod builder_utils;
1313
pub(crate) mod deque;
1414
pub(crate) mod frequency_sketch;
1515
pub(crate) mod time;
16+
pub(crate) mod timer_wheel;
1617

1718
#[cfg(all(test, any(feature = "sync", feature = "future")))]
1819
pub(crate) mod test_utils;

src/common/time.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ pub(crate) trait CheckedTimeOps {
1919
fn checked_add(&self, duration: Duration) -> Option<Self>
2020
where
2121
Self: Sized;
22+
23+
fn checked_duration_since(&self, earlier: Self) -> Option<Duration>
24+
where
25+
Self: Sized;
2226
}
2327

2428
impl Instant {
@@ -40,4 +44,11 @@ impl CheckedTimeOps for Instant {
4044
fn checked_add(&self, duration: Duration) -> Option<Instant> {
4145
self.0.checked_add(duration).map(Instant)
4246
}
47+
48+
fn checked_duration_since(&self, earlier: Self) -> Option<Duration>
49+
where
50+
Self: Sized,
51+
{
52+
self.0.checked_duration_since(earlier.0)
53+
}
4354
}

src/common/timer_wheel.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// License and Copyright Notice:
2+
//
3+
// Some of the code and doc comments in this module were ported or copied from
4+
// a Java class `com.github.benmanes.caffeine.cache.TimerWheel` of Caffeine.
5+
// https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/TimerWheel.java
6+
//
7+
// The original code/comments from Caffeine are licensed under the Apache License,
8+
// Version 2.0 <https://github.com/ben-manes/caffeine/blob/master/LICENSE>
9+
//
10+
// Copyrights of the original code/comments are retained by their contributors.
11+
// For full authorship information, see the version control history of
12+
// https://github.com/ben-manes/caffeine/
13+
14+
#![allow(unused)] // TODO: Remove this.
15+
16+
use std::{convert::TryInto, ptr::NonNull, time::Duration};
17+
18+
use super::{
19+
deque::{DeqNode, Deque},
20+
time::{CheckedTimeOps, Instant},
21+
};
22+
23+
const BUCKET_COUNTS: &[u64] = &[
24+
64, // roughly seconds
25+
64, // roughly minutes
26+
32, // roughly hours
27+
4, // roughly days
28+
1, // overflow (> ~6.5 days)
29+
];
30+
31+
const OVERFLOW_QUEUE_INDEX: usize = BUCKET_COUNTS.len() - 1;
32+
const NUM_LEVELS: usize = OVERFLOW_QUEUE_INDEX - 1;
33+
34+
const DAY: Duration = Duration::from_secs(60 * 60 * 24);
35+
36+
const SPANS: &[u64] = &[
37+
aligned_duration(Duration::from_secs(1)), // 1.07s
38+
aligned_duration(Duration::from_secs(60)), // 1.14m
39+
aligned_duration(Duration::from_secs(60 * 60)), // 1.22h
40+
aligned_duration(DAY), // 1.63d
41+
BUCKET_COUNTS[3] * aligned_duration(DAY), // 6.5d
42+
BUCKET_COUNTS[3] * aligned_duration(DAY), // 6.5d
43+
];
44+
45+
const SHIFT: &[u64] = &[
46+
SPANS[0].trailing_zeros() as u64,
47+
SPANS[1].trailing_zeros() as u64,
48+
SPANS[2].trailing_zeros() as u64,
49+
SPANS[3].trailing_zeros() as u64,
50+
SPANS[4].trailing_zeros() as u64,
51+
];
52+
53+
/// Returns the next power of two of the duration in nanoseconds.
54+
const fn aligned_duration(duration: Duration) -> u64 {
55+
// NOTE: as_nanos() returns u128, so convert it to u64 by using `as`.
56+
// We cannot call TryInto::try_into() here because it is not a const fn.
57+
(duration.as_nanos() as u64).next_power_of_two()
58+
}
59+
60+
/// A hierarchical timer wheel to add, remove, and fire expiration events in
61+
/// amortized O(1) time.
62+
///
63+
/// The expiration events are deferred until the timer is advanced, which is
64+
/// performed as part of the cache's housekeeping cycle.
65+
pub(crate) struct TimerWheel<T> {
66+
wheels: Box<[Box<[Deque<T>]>]>,
67+
/// The time when this timer wheel was created.
68+
origin: Instant,
69+
/// The time when this timer wheel was last advanced.
70+
current: Instant,
71+
}
72+
73+
impl<T> TimerWheel<T> {
74+
fn new(now: Instant) -> Self {
75+
let wheels = BUCKET_COUNTS
76+
.iter()
77+
.map(|b| {
78+
(0..*b)
79+
.map(|_| Deque::new(super::CacheRegion::Other))
80+
.collect::<Vec<_>>()
81+
.into_boxed_slice()
82+
})
83+
.collect::<Vec<_>>()
84+
.into_boxed_slice();
85+
Self {
86+
wheels,
87+
origin: now,
88+
current: now,
89+
}
90+
}
91+
92+
/// Schedules a timer event for the node.
93+
// pub(crate) fn schedule(&mut self, node: Box<DeqNode<T>>) {
94+
// if let Some(t) = node.element.expiration_time() {
95+
// let (level, index) = self.bucket_indices(t);
96+
// self.wheels[level][index].push_back(node);
97+
// }
98+
// }
99+
100+
// /// Reschedules an active timer event for the node.
101+
// pub(crate) fn reschedule(&mut self, node: NonNull<DeqNode<T>>) {}
102+
103+
/// Removes a timer event for this node if present.
104+
// pub(crate) fn deschedule(&mut self, node: NonNull<DeqNode<T>>) {
105+
// if let Some(t) = node.element.expiration_time() {
106+
// let (level, index) = self.bucket_indices(t);
107+
// unsafe { self.wheels[level][index].unlink_and_drop(node) };
108+
// }
109+
// }
110+
111+
/// Returns the bucket indices to locate the bucket that the timer event
112+
/// should be added to.
113+
fn bucket_indices(&self, time: Instant) -> (usize, usize) {
114+
let duration = time
115+
.checked_duration_since(self.current)
116+
// FIXME: unwrap will panic if the time is earlier than self.current.
117+
.unwrap()
118+
.as_nanos() as u64;
119+
// ENHANCEME: Check overflow? (u128 -> u64)
120+
// FIXME: unwrap will panic if the time is earlier than self.origin.
121+
let time_nano = time.checked_duration_since(self.origin).unwrap().as_nanos() as u64;
122+
for level in 0..=NUM_LEVELS {
123+
if duration < SPANS[level + 1] {
124+
let ticks = time_nano >> SHIFT[level];
125+
let index = ticks & (BUCKET_COUNTS[level] - 1);
126+
return (level, index as usize);
127+
}
128+
}
129+
(OVERFLOW_QUEUE_INDEX, 0)
130+
}
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use std::time::Duration;
136+
137+
use super::{TimerWheel, SPANS};
138+
use crate::common::time::{CheckedTimeOps, Clock, Instant};
139+
140+
#[test]
141+
fn test_bucket_indices() {
142+
fn dur(nanos: u64) -> Duration {
143+
Duration::from_nanos(nanos)
144+
}
145+
146+
fn bi(timer: &TimerWheel<()>, now: Instant, dur: Duration) -> (usize, usize) {
147+
let t = now.checked_add(dur).unwrap();
148+
timer.bucket_indices(t)
149+
}
150+
151+
let (clock, mock) = Clock::mock();
152+
let now = Instant::new(clock.now());
153+
154+
let mut timer = TimerWheel::<()>::new(now);
155+
assert_eq!(timer.bucket_indices(now), (0, 0));
156+
157+
// Level 0: 1.07s
158+
assert_eq!(bi(&timer, now, dur(SPANS[0] - 1)), (0, 0));
159+
assert_eq!(bi(&timer, now, dur(SPANS[0])), (0, 1));
160+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 63)), (0, 63));
161+
162+
// Level 1: 1.14m
163+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 64)), (1, 1));
164+
assert_eq!(bi(&timer, now, dur(SPANS[1])), (1, 1));
165+
assert_eq!(bi(&timer, now, dur(SPANS[1] * 63 + SPANS[0] * 63)), (1, 63));
166+
167+
// Level 2: 1.22h
168+
assert_eq!(bi(&timer, now, dur(SPANS[1] * 64)), (2, 1));
169+
assert_eq!(bi(&timer, now, dur(SPANS[2])), (2, 1));
170+
assert_eq!(
171+
bi(
172+
&timer,
173+
now,
174+
dur(SPANS[2] * 31 + SPANS[1] * 63 + SPANS[0] * 63)
175+
),
176+
(2, 31)
177+
);
178+
179+
// Level 3: 1.63dh
180+
assert_eq!(bi(&timer, now, dur(SPANS[2] * 32)), (3, 1));
181+
assert_eq!(bi(&timer, now, dur(SPANS[3])), (3, 1));
182+
assert_eq!(bi(&timer, now, dur(SPANS[3] * 3)), (3, 3));
183+
184+
// Overflow
185+
assert_eq!(bi(&timer, now, dur(SPANS[3] * 4)), (4, 0));
186+
assert_eq!(bi(&timer, now, dur(SPANS[4])), (4, 0));
187+
assert_eq!(bi(&timer, now, dur(SPANS[4] * 100)), (4, 0));
188+
189+
// Increment the clock by 5 ticks. (1 tick ~= 1.07s)
190+
mock.increment(dur(SPANS[0] * 5));
191+
let now = Instant::new(clock.now());
192+
timer.current = now;
193+
194+
// Level 0: 1.07s
195+
assert_eq!(bi(&timer, now, dur(SPANS[0] - 1)), (0, 5));
196+
assert_eq!(bi(&timer, now, dur(SPANS[0])), (0, 6));
197+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 63)), (0, 4));
198+
199+
// Level 1: 1.14m
200+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 64)), (1, 1));
201+
assert_eq!(bi(&timer, now, dur(SPANS[1])), (1, 1));
202+
assert_eq!(
203+
bi(&timer, now, dur(SPANS[1] * 63 + SPANS[0] * (63 - 5))),
204+
(1, 63)
205+
);
206+
207+
// Increment the clock by 61 ticks. (total 66 ticks)
208+
mock.increment(dur(SPANS[0] * 61));
209+
let now = Instant::new(clock.now());
210+
timer.current = now;
211+
212+
// Level 0: 1.07s
213+
assert_eq!(bi(&timer, now, dur(SPANS[0] - 1)), (0, 2));
214+
assert_eq!(bi(&timer, now, dur(SPANS[0])), (0, 3));
215+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 63)), (0, 1));
216+
217+
// Level 1: 1.14m
218+
assert_eq!(bi(&timer, now, dur(SPANS[0] * 64)), (1, 2));
219+
assert_eq!(bi(&timer, now, dur(SPANS[1])), (1, 2));
220+
assert_eq!(
221+
bi(&timer, now, dur(SPANS[1] * 63 + SPANS[0] * (63 - 2))),
222+
(1, 0)
223+
);
224+
}
225+
}

0 commit comments

Comments
 (0)