pub struct CoreMailbox { /* private fields */ }Expand description
Send + Sync mailbox bridging autonomous async producers to an owned,
relocatable crate::node::Core. Held behind an Arc; the Core
owns one clone, each timer task another. See the module docs.
Implementations§
Source§impl CoreMailbox
impl CoreMailbox
Sourcepub fn post_emit(&self, node_id: NodeId, handle: HandleId) -> bool
pub fn post_emit(&self, node_id: NodeId, handle: HandleId) -> bool
Post a timer-fired emit request. Returns false iff the owning
Core has already dropped (Self::close was called) — the
caller MUST then release handle and stop (mirrors the old
WeakCore::upgrade() == None teardown branch in timer.rs).
Returns true when queued (and sets the runnable wake bit).
Sourcepub fn post_complete(&self, node_id: NodeId) -> bool
pub fn post_complete(&self, node_id: NodeId) -> bool
Post a producer-sink Complete (D232-AMEND/A′). Returns false
iff the owning Core is gone (caller stops; nothing to release).
Sourcepub fn post_error(&self, node_id: NodeId, handle: HandleId) -> bool
pub fn post_error(&self, node_id: NodeId, handle: HandleId) -> bool
Post a producer-sink Error (D232-AMEND/A′). Returns false iff
the owning Core is gone — the caller MUST then release
handle (it owned a retain for the would-be error payload).
Sourcepub fn post_defer(&self, f: SendDeferFn) -> bool
pub fn post_defer(&self, f: SendDeferFn) -> bool
Post a Send cross-thread Defer (D249/S2c). For an
autonomous timer task whose closure captures only Send state
(temporal.rs window_time/etc.). Returns false iff the
owning Core is gone — the closure is dropped unrun. The
!Send owner-side sink defers use DeferQueue::post instead.
Sourcepub fn post_op(&self, op: MailboxOp) -> bool
pub fn post_op(&self, op: MailboxOp) -> bool
Post a MailboxOp. Returns false iff the owning Core has
already dropped (Self::close) — see the per-kind wrappers for
the caller’s handle-release obligation.
QA F-A (2026-05-18): the closed check and the push_back are
performed in one ops-lock critical section so a concurrent
owner-thread close() (which also takes ops) cannot interleave
between “observed not-closed” and “enqueued” — that TOCTOU would
strand the op (with its retained HandleId) in a queue
Drop for Core already walked → leak. close() takes the same
lock, so the two are mutually exclusive.
Sourcepub fn drain_into(&self, max_ops: u32, apply: impl FnMut(MailboxOp))
pub fn drain_into(&self, max_ops: u32, apply: impl FnMut(MailboxOp))
Owner-side drain. Pops every queued MailboxOp in FIFO order
and hands each to apply (the caller passes a closure over the
sync Core::{emit,complete,error}). Re-entrancy: apply may
itself cascade and a concurrent timer task / re-entrant sink may
post again — a fresh post re-sets runnable, so the enclosing
drain-to-quiescence loop (or a later drain) picks it up.
QA F-#4 (2026-05-18): the empty observation and the
runnable = false store happen **in the same ops-lock critical
section. Previously the empty pop_front released the lock before
storing false, so a concurrent post_op (which takes ops)
could push and set runnable=true in between, then our false
store would clobber it — a lost wakeup invisible to the
is_runnable-gated in-wave drain and the S4/M6 scheduler.
Clearing runnable while still holding the lock every post_op
must take orders them: a post is either popped here or runs
strictly after the false store and re-sets true.
max_ops bounds a single drain (QA P3, 2026-05-18): apply may
re-post (a Defer that re-defers, a producer re-subscribing),
which this loop correctly drains in the same call — but a
closure that re-posts itself on every application is an
unbounded mailbox livelock (the producer-authoring analogue of a
fn that emits to itself). That livelock lives HERE (the inner
drain loop), not in drain_and_flush’s fire-cascade cap, so it
is bounded + panics here — decoupled from the fire counter so a
legitimately large finite producer drain never false-trips the
fire cap.
/qa M3 (2026-05-19): a panic from apply(f) mid-drain previously
unwound out of this loop with runnable still true — under the
parking_lot::Mutex shape that was self-correcting (the next
drain pop would re-observe + clear), but downstream
is_runnable() gates would spuriously return true until then.
Wraps the empty-queue clear in a RunnableClearGuard so an
apply panic still clears runnable on unwind: if the queue is
empty at unwind time the cell is reset; if the queue is
non-empty the next post would re-set it anyway. Tightens the
scheduler-wakeup contract under panic.
§Panics
Panics if more than max_ops ops are applied in one drain — that
indicates a producer / Defer op re-posting itself every
application (livelock guard). The default cap is sized for
realistic cascades; bump via the corresponding setter if your
workload has evidence it needs more.
Sourcepub fn is_runnable(&self) -> bool
pub fn is_runnable(&self) -> bool
Whether the mailbox currently holds queued work (the wake bit).
Advisory pre-M6 (the embedder pump drains unconditionally); S4/M6
consumers gate scheduling on it. #[must_use] (/qa m4) — a
discarded result silently loses the scheduling signal.
Sourcepub fn close(&self)
pub fn close(&self)
Mark the owning Core gone. Idempotent. Takes the ops lock so
it is mutually exclusive with Self::post_op’s under-lock
closed check (QA F-A): after this returns, no further post_op
can enqueue. Callers MUST then Self::take_all and release any
Emit/Error payload handles still queued (a TOCTOU-enqueued op
posted just before close won the lock).
Sourcepub fn take_all(&self) -> VecDeque<MailboxOp>
pub fn take_all(&self) -> VecDeque<MailboxOp>
Drain and return every still-queued MailboxOp without
applying it — for Drop for Core teardown (QA F-A / Blind #2).
Emit/Error ops carry a retained HandleId the caller must
release; Defer closures are dropped unrun (running CoreFull
on a half-dropped Core is unsound — user-locked QA decision A,
2026-05-18). Clears runnable under the lock (same race
discipline as drain_into).
Sourcepub fn is_closed(&self) -> bool
pub fn is_closed(&self) -> bool
Whether Self::close has been called.