feat(causal): wall-anchored send_after — user-facing timer opt-out (RFC 007)

send_after_wall / send_after_named_wall (+ Timers::insert_send_wall) arm a
message-delivery timer that opts out of the RFC 007 virtual-time shift and
fires at its raw deadline regardless of injected delay — the Send-reason
sibling of sleep_wall, closing the jar item whose substrate efbc254 landed.
For deadlines that reflect the outside world (protocol timeouts, wall-clock
schedules) rather than workload pacing. cancel_timer is anchor-agnostic and
unchanged; without the feature the API exists and is identical to send_after.

The gen_server timer layer (send_after_to, RFC 015 §5) deliberately stays
virtual-only — an opt-out there means new options on the gen_server/statem
timeout API, out of scope for now.

Tests: wall send fires at raw deadline while a virtual sibling shifts;
cancel on a wall send with debt outstanding; featureless delivery/cancel
smokes through the public named API. 15/15 causal, 34/34 binaries both
feature configs, lib clippy clean both.
This commit is contained in:
Claude (sandbox)
2026-07-13 11:10:11 +00:00
parent fec760a3c0
commit a4647f368a
5 changed files with 198 additions and 1 deletions
+45
View File
@@ -497,6 +497,51 @@ pub fn send_after_named<M: Send + 'static>(
})
}
/// Wall-anchored [`send_after`] (RFC 007): under causal profiling the timer
/// opts out of the virtual-time shift and fires at its raw deadline instead
/// of dilating with injected delay — the user-facing opt-out whose substrate
/// [`sleep_wall`] landed. Use it for deadlines that reflect the outside
/// world (protocol timeouts, wall-clock schedules) rather than workload
/// pacing. Without the `smarm-causal` feature it is identical to
/// [`send_after`]. Cancellation via [`cancel_timer`] is unchanged.
pub fn send_after_wall<A: crate::pid::Addressable>(
after: std::time::Duration,
dest: Pid<A>,
msg: A::Msg,
) -> crate::timer::TimerId {
let deadline = crate::timer::deadline_from_now(after);
let fire = Box::new(move || {
let _ = crate::registry::send_to(dest, msg);
});
with_runtime(|inner| {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send_wall(deadline, dest.erase(), fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
}
})
}
/// Wall-anchored [`send_after_named`] (RFC 007): re-resolving name delivery,
/// raw-deadline anchor — see [`send_after_wall`] for the semantics.
pub fn send_after_named_wall<M: Send + 'static>(
after: std::time::Duration,
dest: Name<M>,
msg: M,
) -> crate::timer::TimerId {
let deadline = crate::timer::deadline_from_now(after);
// Informational only (who armed it); not used for delivery.
let armed_by = current_pid().unwrap_or(Pid::new(0, 0));
let fire = Box::new(move || {
let _ = crate::registry::send(dest, msg);
});
with_runtime(|inner| {
match inner.timers.lock() {
Ok(mut timers) => timers.insert_send_wall(deadline, armed_by, fire),
Err(e) => panic!("smarm: timers lock poisoned (core corrupt): {e}"),
}
})
}
/// Deliver `msg` onto a channel the caller owns after `after`, rather than to a
/// registry address. The sibling of [`send_after`] used by the gen_server timer
/// layer (RFC 015 §5): arming a server timer must land the fire on the loop's