timer: send_after / cancel_timer message-delivery substrate
Add a cancellable message-delivery timer on the existing timer.rs min-heap,
the substrate the gen_server time idioms (idle/receive timeout, periodic tick,
debounce/backoff) need.
- Reason::Send { fire }: a type-erased delivery thunk. send_after (Pid<A>, via
send_to) and send_after_named (Name<M>, via send) capture dest+msg and
resolve the address on fire, not at arm time, so a dead target / restarted
name is observed when it fires; a failed resolve or closed inbox is dropped
(Erlang erlang:send_after semantics).
- Cancellation via an "armed" set keyed on the entry seq, exposed as an opaque
TimerId. Only Send timers use it; Sleep/WaitTimeout keep their inert-stale
behaviour untouched. pop_due fires a Send only while still armed and removes
it, so cancel returns true iff it landed before the fire (the race signal).
cancel is unscoped: any holder of the id can cancel (e.g. racing two servers
and cancelling the slow path).
- peek_deadline contract documented as "<= true next deadline" so a future
hierarchical timing wheel can back Timers without touching send_after or the
scheduler idle path. call_timeout left on recv_timeout (blast radius).
Tests: Timers-level (fire/cancel/race/clear/ordering) plus scheduler-level
delivery for both Name<M> and Pid<A>, cancel-prevents-delivery, and silent
drop on unresolved name / dead pid. Green on rq-mutex/rq-mpmc/rq-striped.
This commit is contained in:
+200
@@ -205,3 +205,203 @@ fn same_deadline_entries_pop_in_insertion_order() {
|
||||
let pids: Vec<u32> = due.iter().map(|e| e.pid.index()).collect();
|
||||
assert_eq!(pids, vec![0, 1, 2]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// send_after / cancel_timer — the message-delivery timer substrate.
|
||||
//
|
||||
// Unit tests drive `Timers` directly with a flag-flipping fire thunk (no
|
||||
// runtime needed, mirroring RecordingTarget above). Integration tests drive
|
||||
// the public scheduler API and assert real registry-resolved delivery.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
// Pull the Send fire thunk out of a popped entry and run it.
|
||||
fn run_fire(entry: smarm::timer::Entry) {
|
||||
match entry.reason {
|
||||
Reason::Send { fire } => fire(),
|
||||
_ => panic!("expected a Send entry"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn armed_send_timer_is_returned_and_fires() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let fired = Arc::new(AtomicBool::new(false));
|
||||
let f = fired.clone();
|
||||
let _id = t.insert_send(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(0, 0),
|
||||
Box::new(move || f.store(true, Ordering::SeqCst)),
|
||||
);
|
||||
|
||||
let mut due = t.pop_due(now + Duration::from_millis(20));
|
||||
assert_eq!(due.len(), 1, "an armed send timer should pop when due");
|
||||
assert!(!fired.load(Ordering::SeqCst), "pop must not fire on its own");
|
||||
run_fire(due.pop().unwrap());
|
||||
assert!(fired.load(Ordering::SeqCst), "running the thunk delivers");
|
||||
assert!(t.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelled_send_timer_is_discarded_not_returned() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let fired = Arc::new(AtomicBool::new(false));
|
||||
let f = fired.clone();
|
||||
let id = t.insert_send(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(0, 0),
|
||||
Box::new(move || f.store(true, Ordering::SeqCst)),
|
||||
);
|
||||
|
||||
assert!(t.cancel(id), "cancel before fire returns true");
|
||||
let due = t.pop_due(now + Duration::from_millis(20));
|
||||
assert!(due.is_empty(), "a cancelled send timer must not pop");
|
||||
assert!(!fired.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_after_fire_returns_false() {
|
||||
// The race signal Mark wanted: cancelling a timer that already fired tells
|
||||
// you it was too late.
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let id = t.insert_send(
|
||||
now + Duration::from_millis(5),
|
||||
Pid::new(0, 0),
|
||||
Box::new(|| {}),
|
||||
);
|
||||
let due = t.pop_due(now + Duration::from_millis(10));
|
||||
assert_eq!(due.len(), 1);
|
||||
assert!(!t.cancel(id), "cancel after the timer fired returns false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_unknown_id_returns_false() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let id = t.insert_send(now + Duration::from_millis(5), Pid::new(0, 0), Box::new(|| {}));
|
||||
assert!(t.cancel(id));
|
||||
// Second cancel of the same id: already gone.
|
||||
assert!(!t.cancel(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_timers_interleave_with_sleep_in_deadline_order() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1);
|
||||
let _id = t.insert_send(now + Duration::from_millis(10), Pid::new(1, 0), Box::new(|| {}));
|
||||
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1);
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(50));
|
||||
assert_eq!(due.len(), 3);
|
||||
// 10ms Send, then 20ms Sleep, then 30ms Sleep.
|
||||
assert!(matches!(due[0].reason, Reason::Send { .. }));
|
||||
assert_eq!(due[1].pid.index(), 2);
|
||||
assert_eq!(due[2].pid.index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_drops_armed_send_timers() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let id = t.insert_send(now + Duration::from_millis(10), Pid::new(0, 0), Box::new(|| {}));
|
||||
t.clear();
|
||||
assert!(t.is_empty());
|
||||
// The arm record is gone too: cancelling reports nothing to cancel.
|
||||
assert!(!t.cancel(id));
|
||||
}
|
||||
|
||||
// --- Integration: real delivery through the scheduler + registry. ---
|
||||
|
||||
use smarm::{cancel_timer, channel, register, send_after_named, Name};
|
||||
|
||||
#[test]
|
||||
fn send_after_named_delivers_after_the_delay() {
|
||||
const PING: Name<u64> = Name::new("send_after_ping");
|
||||
run(|| {
|
||||
let (tx, rx) = channel::<u64>();
|
||||
register(PING, tx).unwrap();
|
||||
let t0 = Instant::now();
|
||||
let _id = send_after_named(Duration::from_millis(30), PING, 99);
|
||||
assert_eq!(rx.recv().unwrap(), 99);
|
||||
assert!(
|
||||
t0.elapsed() >= Duration::from_millis(25),
|
||||
"delivered too early: {:?}",
|
||||
t0.elapsed()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_timer_prevents_delivery() {
|
||||
const C: Name<u64> = Name::new("send_after_cancel");
|
||||
run(|| {
|
||||
let (tx, rx) = channel::<u64>();
|
||||
register(C, tx).unwrap();
|
||||
let id = send_after_named(Duration::from_millis(50), C, 7);
|
||||
assert!(cancel_timer(id), "cancel before fire returns true");
|
||||
sleep(Duration::from_millis(90));
|
||||
assert_eq!(rx.try_recv(), Ok(None), "cancelled timer delivered anyway");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_after_to_unresolved_name_is_silent() {
|
||||
const NOPE: Name<u64> = Name::new("send_after_nobody_home");
|
||||
run(|| {
|
||||
// Nobody registered NOPE; firing resolves to nothing and is dropped.
|
||||
let _id = send_after_named(Duration::from_millis(10), NOPE, 1);
|
||||
sleep(Duration::from_millis(40)); // let it fire and no-op
|
||||
// Reaching here without a panic is the assertion.
|
||||
});
|
||||
}
|
||||
|
||||
// --- Integration: typed Pid<A> delivery (exercises send_to on fire). ---
|
||||
|
||||
use smarm::{send_after, send_to, spawn_addr, Addressable, Receiver};
|
||||
|
||||
struct Sink;
|
||||
impl Addressable for Sink {
|
||||
type Msg = u64;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_after_delivers_to_typed_pid() {
|
||||
run(|| {
|
||||
// A reply channel so the test actor learns what Sink received.
|
||||
let (report_tx, report_rx) = channel::<u64>();
|
||||
let sink: Pid<Sink> = spawn_addr::<Sink>(move |rx: Receiver<u64>| {
|
||||
if let Ok(v) = rx.recv() {
|
||||
let _ = report_tx.send(v);
|
||||
}
|
||||
});
|
||||
let _id = send_after(Duration::from_millis(25), sink, 1234);
|
||||
assert_eq!(report_rx.recv().unwrap(), 1234);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_after_to_dead_typed_pid_is_silent() {
|
||||
run(|| {
|
||||
// Sink exits immediately after handling one message; arm a second
|
||||
// delivery for after it's gone. The fire-time send_to returns Dead and
|
||||
// is dropped — no panic.
|
||||
let (report_tx, report_rx) = channel::<u64>();
|
||||
let sink: Pid<Sink> = spawn_addr::<Sink>(move |rx: Receiver<u64>| {
|
||||
if let Ok(v) = rx.recv() {
|
||||
let _ = report_tx.send(v);
|
||||
}
|
||||
// body returns -> actor exits
|
||||
});
|
||||
send_to(sink, 1).unwrap();
|
||||
assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited
|
||||
let _id = send_after(Duration::from_millis(15), sink, 2);
|
||||
sleep(Duration::from_millis(45)); // let it fire against the dead pid
|
||||
// No panic, and nothing further delivered.
|
||||
assert_eq!(report_rx.try_recv(), Ok(None));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user