use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use super::callback_executor::{Callback, CallbackHandle, CallbackPriority};
use super::facility::{recover, run_facility_loop, run_isolated};
use crate::runtime::task::{ThreadPriority, enter_ioc_thread};
const FACILITY: &str = "delayed-callback timer";
enum TimerAction {
Inline,
Pool(CallbackPriority),
}
struct TimerEntry {
deadline: Instant,
seq: u64,
action: TimerAction,
cb: Callback,
}
impl PartialEq for TimerEntry {
fn eq(&self, other: &Self) -> bool {
self.deadline == other.deadline && self.seq == other.seq
}
}
impl Eq for TimerEntry {}
impl Ord for TimerEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.deadline
.cmp(&self.deadline)
.then_with(|| other.seq.cmp(&self.seq))
}
}
impl PartialOrd for TimerEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
struct TimerState {
heap: BinaryHeap<TimerEntry>,
next_seq: u64,
shutdown: bool,
}
struct Inner {
state: Mutex<TimerState>,
wake: Condvar,
sink: CallbackHandle,
}
impl Inner {
fn schedule(&self, delay: Duration, action: TimerAction, cb: Callback) {
let deadline = Instant::now() + delay;
let mut st = recover(FACILITY, self.state.lock());
if st.shutdown {
drop(st);
tracing::trace!(
target: "epics_base_rs::runtime::delayed_timer",
"callbackRequestDelayed after shutdown dropped"
);
return;
}
let seq = st.next_seq;
st.next_seq += 1;
st.heap.push(TimerEntry {
deadline,
seq,
action,
cb,
});
drop(st);
self.wake.notify_one();
}
}
fn timer_loop(inner: &Inner) {
let mut st = recover(FACILITY, inner.state.lock());
loop {
if st.shutdown {
return;
}
let now = Instant::now();
match st.heap.peek().map(|e| e.deadline) {
Some(deadline) if deadline <= now => {
let entry = st.heap.pop().unwrap();
drop(st);
match entry.action {
TimerAction::Inline => {
run_isolated(FACILITY, entry.cb);
}
TimerAction::Pool(priority) => {
let _ = inner.sink.request(priority, entry.cb);
}
}
st = recover(FACILITY, inner.state.lock());
}
Some(deadline) => {
let wait = deadline.saturating_duration_since(now);
let (guard, _timeout) = recover(FACILITY, inner.wake.wait_timeout(st, wait));
st = guard;
}
None => {
st = recover(FACILITY, inner.wake.wait(st));
}
}
}
}
#[derive(Clone)]
pub struct TimerHandle {
inner: Arc<Inner>,
}
impl TimerHandle {
pub fn schedule(&self, delay: Duration, priority: CallbackPriority, cb: Callback) {
self.inner.schedule(delay, TimerAction::Pool(priority), cb);
}
pub fn schedule_wake(&self, delay: Duration, cb: Callback) {
self.inner.schedule(delay, TimerAction::Inline, cb);
}
}
pub struct DelayedTimer {
inner: Arc<Inner>,
worker: Option<JoinHandle<()>>,
}
impl DelayedTimer {
pub fn new(sink: CallbackHandle) -> Self {
let inner = Arc::new(Inner {
state: Mutex::new(TimerState {
heap: BinaryHeap::new(),
next_seq: 0,
shutdown: false,
}),
wake: Condvar::new(),
sink,
});
let worker_inner = Arc::clone(&inner);
let worker = std::thread::Builder::new()
.name("cbTimer".to_string())
.stack_size(crate::runtime::task::StackSizeClass::Medium.bytes())
.spawn(move || {
let _ = enter_ioc_thread(ThreadPriority::ScanHigh);
run_facility_loop(
FACILITY,
|| timer_loop(&worker_inner),
|| recover(FACILITY, worker_inner.state.lock()).shutdown = true,
);
})
.expect("failed to spawn delayed-callback timer thread");
DelayedTimer {
inner,
worker: Some(worker),
}
}
pub fn handle(&self) -> TimerHandle {
TimerHandle {
inner: Arc::clone(&self.inner),
}
}
pub fn schedule(&self, delay: Duration, priority: CallbackPriority, cb: Callback) {
self.inner.schedule(delay, TimerAction::Pool(priority), cb);
}
}
impl Drop for DelayedTimer {
fn drop(&mut self) {
{
let mut st = recover(FACILITY, self.inner.state.lock());
st.shutdown = true;
}
self.inner.wake.notify_all();
if let Some(w) = self.worker.take() {
let _ = w.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::background::callback_executor::CallbackPool;
use std::sync::mpsc;
const T: Duration = Duration::from_secs(5);
#[test]
fn delayed_callback_fires_no_earlier_than_delay() {
let pool = CallbackPool::new();
let timer = DelayedTimer::new(pool.handle());
let delay = Duration::from_millis(80);
let (tx, rx) = mpsc::channel();
let start = Instant::now();
timer.schedule(
delay,
CallbackPriority::High,
Box::new(move || tx.send(Instant::now()).unwrap()),
);
let fired_at = rx.recv_timeout(T).unwrap();
assert!(
fired_at.duration_since(start) >= delay,
"callback fired after {:?}, earlier than the {:?} delay",
fired_at.duration_since(start),
delay
);
}
#[test]
fn earlier_deadline_fires_before_later_one() {
let pool = CallbackPool::new();
let timer = DelayedTimer::new(pool.handle());
let (tx, rx) = mpsc::channel();
let tx_long = tx.clone();
timer.schedule(
Duration::from_millis(150),
CallbackPriority::Medium,
Box::new(move || tx_long.send("long").unwrap()),
);
timer.schedule(
Duration::from_millis(30),
CallbackPriority::Medium,
Box::new(move || tx.send("short").unwrap()),
);
assert_eq!(rx.recv_timeout(T).unwrap(), "short");
assert_eq!(rx.recv_timeout(T).unwrap(), "long");
}
#[test]
fn a_panicking_wake_does_not_stop_the_timer() {
let pool = CallbackPool::new();
let timer = DelayedTimer::new(pool.handle());
let h = timer.handle();
h.schedule_wake(
Duration::from_millis(10),
Box::new(|| panic!("a waker panicked on the timer thread")),
);
let (tx, rx) = mpsc::channel();
timer.schedule(
Duration::from_millis(40),
CallbackPriority::High,
Box::new(move || tx.send(()).unwrap()),
);
assert_eq!(
rx.recv_timeout(T),
Ok(()),
"the deadline after a panicking wake never fired: the timer thread died with it"
);
}
#[test]
fn a_poisoned_state_still_schedules_and_fires() {
let pool = CallbackPool::new();
let timer = DelayedTimer::new(pool.handle());
let inner = Arc::clone(&timer.inner);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _held = inner.state.lock().expect("first lock");
panic!("poison the timer state");
}));
assert!(
timer.inner.state.lock().is_err(),
"the state mutex must actually be poisoned for this to test anything"
);
let (tx, rx) = mpsc::channel();
timer.schedule(
Duration::from_millis(10),
CallbackPriority::High,
Box::new(move || tx.send(()).unwrap()),
);
assert_eq!(
rx.recv_timeout(T),
Ok(()),
"a poisoned state stopped the timer facility"
);
}
#[test]
fn schedule_after_shutdown_never_fires() {
let pool = CallbackPool::new();
let timer = DelayedTimer::new(pool.handle());
let h = timer.handle();
drop(timer);
let (tx, rx) = mpsc::channel::<()>();
h.schedule(
Duration::from_millis(0),
CallbackPriority::High,
Box::new(move || tx.send(()).unwrap()),
);
assert_eq!(
rx.recv_timeout(Duration::from_millis(200)),
Err(mpsc::RecvTimeoutError::Disconnected),
"a delayed callback fired after shutdown; it must be dropped"
);
}
}