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(),
}));
let _reactor_thread = thread::Builder::new()
.name("hyphae-timer-reactor".into())
.spawn(move || run_reactor(reactor));
reactor
})
}
fn register(entry: TimerEntry) {
let reactor = reactor();
reactor
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.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 = entry.count.saturating_add(1);
entry.next_fire = entry
.next_fire
.checked_add(entry.period)
.unwrap_or(entry.next_fire);
let now = Instant::now();
if entry.next_fire < now {
let elapsed = now.saturating_duration_since(entry.next_fire).as_nanos();
let missed = elapsed
.checked_div(entry.period.as_nanos().max(1))
.and_then(|value| u64::try_from(value).ok())
.unwrap_or(u64::MAX);
entry.count = entry.count.saturating_add(missed);
entry.next_fire = now.checked_add(entry.period).unwrap_or(now);
}
reactor
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(entry);
}
}
}
fn wait_for_due(reactor: &'static Reactor) -> Vec<TimerEntry> {
let mut guard = reactor
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
loop {
if guard.is_empty() {
guard = reactor
.wake
.wait(guard)
.unwrap_or_else(std::sync::PoisonError::into_inner);
continue;
}
let now = Instant::now();
let Some(next_deadline) = guard.iter().map(|entry| entry.next_fire).min() else {
continue;
};
if next_deadline <= now {
let now = Instant::now();
return drain_due(&mut guard, now);
}
let wait_for = next_deadline.saturating_duration_since(now);
if wait_for > SPIN_THRESHOLD {
let Some(park_for) = wait_for.checked_sub(SPIN_THRESHOLD) else {
continue;
};
let (g, _timed_out) = reactor
.wake
.wait_timeout(guard, park_for)
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard = g;
continue;
}
drop(guard);
spin_sleep::sleep(wait_for);
guard = reactor
.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
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.get(idx).is_some_and(|entry| entry.next_fire <= now) {
due.push(entries.swap_remove(idx));
} else {
idx = idx.saturating_add(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()
.checked_add(delay)
.unwrap_or_else(Instant::now),
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()
.checked_add(period)
.unwrap_or_else(Instant::now),
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(|window| matches!(window, [left, right] if left <= right)),
"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!(matches!(due.as_slice(), [first, second] if first.next_fire <= second.next_fire));
assert_eq!(entries.len(), 1, "the far-future entry stays queued");
}
}