pub use std::time::Instant;
use std::{
sync::{Condvar, Mutex, OnceLock},
thread,
time::Duration,
};
use rayon::prelude::*;
const SPIN_THRESHOLD: Duration = Duration::from_millis(2);
struct TimerEntry {
next_fire: Instant,
period: Duration,
count: u64,
tick: Box<dyn FnMut(u64) -> bool + Send>,
}
struct Reactor {
entries: Mutex<Vec<TimerEntry>>,
wake: Condvar,
}
fn reactor() -> &'static Reactor {
static REACTOR: OnceLock<&'static Reactor> = OnceLock::new();
REACTOR.get_or_init(|| {
let reactor: &'static Reactor = Box::leak(Box::new(Reactor {
entries: Mutex::new(Vec::new()),
wake: Condvar::new(),
}));
thread::Builder::new()
.name("hyphae-timer-reactor".into())
.spawn(move || run_reactor(reactor))
.expect("failed to spawn hyphae timer reactor thread");
reactor
})
}
fn register(entry: TimerEntry) {
let reactor = reactor();
reactor.entries.lock().unwrap().push(entry);
reactor.wake.notify_all();
}
fn run_reactor(reactor: &'static Reactor) -> ! {
loop {
let due = wait_for_due(reactor);
for mut entry in due {
let keep = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(entry.tick)(entry.count)
}))
.unwrap_or(false);
if !keep {
continue;
}
entry.count += 1;
entry.next_fire += entry.period;
let now = Instant::now();
if entry.next_fire < now {
let missed =
((now - entry.next_fire).as_nanos() / entry.period.as_nanos().max(1)) as u64;
entry.count += missed;
entry.next_fire = now + entry.period;
}
reactor.entries.lock().unwrap().push(entry);
}
}
}
fn wait_for_due(reactor: &'static Reactor) -> Vec<TimerEntry> {
let mut guard = reactor.entries.lock().unwrap();
loop {
if guard.is_empty() {
guard = reactor.wake.wait(guard).unwrap();
continue;
}
let now = Instant::now();
let next_deadline = guard.iter().map(|e| e.next_fire).min().expect("non-empty");
if next_deadline <= now {
let now = Instant::now();
return drain_due(&mut guard, now);
}
let wait_for = next_deadline - now;
if wait_for > SPIN_THRESHOLD {
let (g, _timed_out) = reactor
.wake
.wait_timeout(guard, wait_for - SPIN_THRESHOLD)
.unwrap();
guard = g;
continue;
}
drop(guard);
spin_sleep::sleep(wait_for);
guard = reactor.entries.lock().unwrap();
}
}
fn drain_due(entries: &mut Vec<TimerEntry>, now: Instant) -> Vec<TimerEntry> {
let mut due = Vec::new();
let mut idx = 0;
while idx < entries.len() {
if entries[idx].next_fire <= now {
due.push(entries.swap_remove(idx));
} else {
idx += 1;
}
}
due.sort_by_key(|e| e.next_fire);
due
}
pub fn spawn_delayed(delay: Duration, f: impl FnOnce() + Send + 'static) {
let mut f = Some(f);
register(TimerEntry {
next_fire: Instant::now() + delay,
period: delay,
count: 1,
tick: Box::new(move |_count| {
if let Some(f) = f.take() {
f();
}
false
}),
});
}
pub fn spawn_interval(
period: Duration,
_precise: bool,
tick: impl FnMut(u64) -> bool + Send + 'static,
) {
register(TimerEntry {
next_fire: Instant::now() + period,
period,
count: 1,
tick: Box::new(tick),
});
}
pub fn par_for_each<T: Sync>(items: &[T], f: impl Fn(&T) + Send + Sync) {
items.par_iter().for_each(f);
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(next_fire: Instant) -> TimerEntry {
TimerEntry {
next_fire,
period: Duration::from_secs(1),
count: 1,
tick: Box::new(|_| false),
}
}
#[test]
fn drain_due_returns_ascending_deadline_order() {
let base = Instant::now();
let mut entries = vec![
entry(base + Duration::from_millis(30)),
entry(base + Duration::from_millis(10)),
entry(base + Duration::from_millis(20)),
];
let due = drain_due(&mut entries, base + Duration::from_millis(100));
let order: Vec<Instant> = due.iter().map(|e| e.next_fire).collect();
assert!(
order.windows(2).all(|w| w[0] <= w[1]),
"due batch not in ascending next_fire order: reactor would fire a same-cell pair stale-last"
);
assert_eq!(due.len(), 3, "every due entry should be drained");
assert!(
entries.is_empty(),
"drained entries must be removed from the queue"
);
}
#[test]
fn drain_due_leaves_future_entries_queued() {
let base = Instant::now();
let mut entries = vec![
entry(base + Duration::from_millis(50)),
entry(base + Duration::from_millis(10)),
entry(base + Duration::from_secs(10)),
];
let due = drain_due(&mut entries, base + Duration::from_millis(100));
assert_eq!(due.len(), 2, "only the two past-deadline entries are due");
assert!(
due[0].next_fire <= due[1].next_fire,
"due batch must be sorted"
);
assert_eq!(entries.len(), 1, "the far-future entry stays queued");
}
}