Skip to main content

liminal/channel/
subscription.rs

1//! LIM-002 R2/R3: subscriptions backed by real beamr processes.
2//!
3//! Each subscription owns a real, scheduler-supervised beamr native process
4//! (a [`SubscriberProcess`]) plus the in-memory inbox the channel actor delivers
5//! matching envelopes into. The channel actor LINKS to this process's pid on
6//! `Subscribe`; when the [`SubscriptionHandle`] is dropped (or the caller
7//! unsubscribes) the process is terminated, the link fires an `{EXIT, pid, _}`
8//! signal, and the trapping channel actor removes the dead subscriber from its
9//! fan-out list. There is NO weak-Arc polling: liveness is observed structurally
10//! through the beamr link/EXIT path, exactly as the conversation actor observes
11//! its participants (`conversation/actor/beam.rs`).
12//!
13//! R3 predicates live INSIDE the channel actor process: a [`SubscriptionPredicate`]
14//! is a boxed `Fn(&Envelope) -> bool` owned by the actor's subscriber
15//! registration and evaluated at delivery time. This mirrors the participant
16//! `behaviour` pattern (a boxed trait object the process owns); for an in-memory
17//! ephemeral channel there is no need for a serialisable predicate, so a closure
18//! the actor holds is the simplest faithful design.
19
20use std::collections::VecDeque;
21use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
22use std::sync::{Arc, Mutex};
23
24use beamr::native::native_process::{NativeContext, NativeHandler, NativeOutcome};
25use beamr::process::ExitReason;
26use beamr::scheduler::Scheduler;
27use beamr::term::binary_ref::BinaryRef;
28
29use crate::channel::wire::{decode_envelope, encode_envelope};
30use crate::envelope::Envelope;
31use crate::error::LiminalError;
32
33/// A shared, cloneable in-memory inbox a subscriber receives delivered envelopes
34/// on. See [`SubscriptionInbox`].
35pub(crate) type SubscriberInbox = Arc<SubscriptionInbox>;
36
37/// A wake callback fired on EVERY envelope admitted to the inbox (R3, §1.2(2)).
38///
39/// The server installs one that fires the CONNECTION scheduler's `READY` marker,
40/// so a publish into a parked connection's inbox wakes it. It is called from the
41/// PUBLISHING actor's slice (the channel actor for local delivery, the subscriber
42/// process for a remote frame), so it must be cheap and non-blocking — a single
43/// `enqueue_atom_message`. `None` (no notifier installed) is the standalone
44/// library / test case: nothing to wake, delivery still lands in the inbox.
45pub type InboxNotifier = Arc<dyn Fn() + Send + Sync>;
46
47/// One shared inbox-byte budget per connection (§5).
48///
49/// Spent across ALL that connection's subscription inboxes. The accounting unit is
50/// serialized envelope bytes AS ADMITTED — charged at enqueue, released at dequeue
51/// — so the signed 4 MiB product is exact and envelope-size-independent, not a
52/// per-inbox count bounding a variable the design does not control.
53#[derive(Debug)]
54pub struct ConnectionInboxBudget {
55    used: AtomicUsize,
56    cap: usize,
57}
58
59impl ConnectionInboxBudget {
60    /// Creates a shared budget with `cap` bytes of headroom across all the
61    /// connection's inboxes.
62    #[must_use]
63    pub fn new(cap: usize) -> Arc<Self> {
64        Arc::new(Self {
65            used: AtomicUsize::new(0),
66            cap,
67        })
68    }
69
70    /// Attempts to charge `bytes`. Returns `true` and reserves the bytes when they
71    /// fit within the remaining budget, `false` (reserving nothing) on overflow. A
72    /// CAS loop keeps the reservation exact under concurrent charges from several
73    /// inboxes — no transient over-charge is ever observable.
74    fn try_charge(&self, bytes: usize) -> bool {
75        let mut current = self.used.load(Ordering::Acquire);
76        loop {
77            let Some(projected) = current.checked_add(bytes) else {
78                return false;
79            };
80            if projected > self.cap {
81                return false;
82            }
83            match self.used.compare_exchange_weak(
84                current,
85                projected,
86                Ordering::AcqRel,
87                Ordering::Acquire,
88            ) {
89                Ok(_) => return true,
90                Err(observed) => current = observed,
91            }
92        }
93    }
94
95    /// Releases `bytes` previously charged (at dequeue). Saturating so a double
96    /// release can never wrap the counter below zero.
97    fn release(&self, bytes: usize) {
98        let mut current = self.used.load(Ordering::Acquire);
99        loop {
100            let next = current.saturating_sub(bytes);
101            match self.used.compare_exchange_weak(
102                current,
103                next,
104                Ordering::AcqRel,
105                Ordering::Acquire,
106            ) {
107                Ok(_) => return,
108                Err(observed) => current = observed,
109            }
110        }
111    }
112
113    /// Bytes currently reserved across the connection's inboxes.
114    #[cfg(test)]
115    pub(crate) fn used(&self) -> usize {
116        self.used.load(Ordering::Acquire)
117    }
118}
119
120/// Everything a server connection installs onto a subscription's inbox.
121///
122/// Carries the shared §5 byte budget, the per-inbox fairness cap, and the R3
123/// wake notifier. Passed INTO the subscribe call so the installation happens at
124/// inbox construction — strictly BEFORE the registration is published to the
125/// channel actor — closing the pre-install window in which envelopes could be
126/// admitted uncharged or without a wake.
127pub struct InboxInstall {
128    /// Shared per-connection byte budget (§5).
129    pub budget: Arc<ConnectionInboxBudget>,
130    /// Per-inbox envelope-count fairness trip (§5).
131    pub depth_cap: usize,
132    /// R3 wake notifier fired on every envelope admitted to the inbox. `None`
133    /// when the caller has no waker (scheduler-free unit tests).
134    pub notifier: Option<InboxNotifier>,
135}
136
137impl std::fmt::Debug for InboxInstall {
138    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        formatter
140            .debug_struct("InboxInstall")
141            .field("depth_cap", &self.depth_cap)
142            .field("has_notifier", &self.notifier.is_some())
143            .finish_non_exhaustive()
144    }
145}
146
147/// Mutable inbox state guarded by one lock: the queued envelopes (each carrying
148/// the exact bytes CHARGED for it, so release is symmetric with charge), the
149/// installed shared budget and per-inbox fairness cap, the wake notifier, and
150/// the closed marker.
151struct InboxState {
152    /// Each entry is `(envelope, charged_bytes)` — the amount actually charged to
153    /// the shared budget at enqueue (0 when no budget was installed at admit
154    /// time), released verbatim at dequeue/close. Storing the CHARGE, not the
155    /// size, makes release byte-identical to charge on every entry even across a
156    /// budget install, so the budget can never under- or over-release.
157    queue: VecDeque<(Envelope, usize)>,
158    /// Shared per-connection byte budget (§5). `None` = unbounded (standalone
159    /// library use / tests), preserving the pre-bounding behaviour exactly.
160    budget: Option<Arc<ConnectionInboxBudget>>,
161    /// Per-inbox envelope-count secondary fairness trip (§5). `usize::MAX` = off;
162    /// stops one subscription starving its siblings inside the shared byte budget.
163    depth_cap: usize,
164    /// Wake callback (R3). `None` until the connection installs one.
165    notifier: Option<InboxNotifier>,
166    /// Terminal marker set by [`SubscriptionInbox::close`]: admissions are refused
167    /// WITHOUT charging, and all queued charges have been released. Closing is the
168    /// release-by-construction seam — every teardown path (explicit unsubscribe,
169    /// overflow shed, connection teardown, and the `Drop` backstop) funnels
170    /// through it, so queued bytes can never be stranded on the connection-lifetime
171    /// budget.
172    closed: bool,
173}
174
175/// The shared subscription inbox (R3 + §5). Replaces the bare
176/// `Arc<Mutex<VecDeque<Envelope>>>`: it fires a wake notifier on every admitted
177/// envelope and enforces the connection-scoped byte budget plus the per-inbox
178/// fairness trip, shedding the offending subscription on overflow.
179pub(crate) struct SubscriptionInbox {
180    state: Mutex<InboxState>,
181    /// Sticky overflow marker: set when an admission is refused by the byte budget
182    /// or the fairness trip. The server-side delivery pump observes it and sheds
183    /// this subscription with a typed error frame, mirroring the outbound overflow
184    /// policy (a slow consumer sheds its own subscription; it cannot grow server
185    /// memory without bound). Sticky (never cleared) because a shed is terminal.
186    overflowed: AtomicBool,
187}
188
189impl std::fmt::Debug for SubscriptionInbox {
190    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        formatter
192            .debug_struct("SubscriptionInbox")
193            .field("overflowed", &self.overflowed.load(Ordering::Acquire))
194            .finish_non_exhaustive()
195    }
196}
197
198/// Why an inbox admission was refused (§5). The budget/fairness refusals set the
199/// sticky overflow marker and drop the envelope rather than growing memory; a
200/// closed inbox refuses without charging and without marking.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub(crate) enum InboxAdmission {
203    /// The envelope was admitted (and, when it made the inbox non-empty, the wake
204    /// notifier was fired).
205    Admitted,
206    /// The shared connection byte budget (§5) had no room; the subscription is
207    /// shed.
208    BudgetExceeded,
209    /// The per-inbox fairness trip (§5) is full; the subscription is shed.
210    FairnessTripped,
211    /// The inbox was closed (unsubscribe/shed/teardown): the envelope is dropped
212    /// without charging the budget — a closed inbox can never re-accumulate cost.
213    Closed,
214}
215
216impl SubscriptionInbox {
217    /// Creates an unbounded, notifier-less inbox — the standalone/default shape,
218    /// byte-identical to the pre-bounding behaviour. A server connection passes an
219    /// [`InboxInstall`] through subscribe so budget/cap/notifier are installed at
220    /// construction instead.
221    pub(crate) fn new() -> Arc<Self> {
222        Arc::new(Self {
223            state: Mutex::new(InboxState {
224                queue: VecDeque::new(),
225                budget: None,
226                depth_cap: usize::MAX,
227                notifier: None,
228                closed: false,
229            }),
230            overflowed: AtomicBool::new(false),
231        })
232    }
233
234    /// Installs the connection's shared byte budget and per-inbox fairness cap
235    /// (§5). Runs at inbox construction (via [`InboxInstall`]) — before the
236    /// registration is published to the channel actor — so no envelope can be
237    /// admitted uncharged.
238    pub(crate) fn install_budget(&self, budget: Arc<ConnectionInboxBudget>, depth_cap: usize) {
239        if let Ok(mut state) = self.state.lock() {
240            state.budget = Some(budget);
241            state.depth_cap = depth_cap;
242        }
243    }
244
245    /// Installs the wake notifier (R3), fired on every admitted envelope,
246    /// capturing the connection scheduler's enqueue handle (§1.2(2)).
247    ///
248    /// Defensive invariant: the install RECHECKS non-emptiness under the lock and
249    /// fires the notifier (outside the lock) when envelopes are already queued —
250    /// those envelopes were admitted while there was no notifier to fire, so
251    /// without this recheck their wake would be lost to install ordering. On the
252    /// normal construction path the queue is empty and this is a no-op.
253    pub(crate) fn install_notifier(&self, notifier: InboxNotifier) {
254        let fire = {
255            let Ok(mut state) = self.state.lock() else {
256                return;
257            };
258            let pending = !state.queue.is_empty();
259            let handle = notifier.clone();
260            state.notifier = Some(notifier);
261            pending.then_some(handle)
262        };
263        if let Some(notifier) = fire {
264            notifier();
265        }
266    }
267
268    /// Admits `envelope` under the byte budget and fairness trip, charging the
269    /// serialized bytes as admitted and firing the wake notifier for EVERY
270    /// admitted envelope (level-triggered — see the fire site below for why the
271    /// edge-triggered form starved a subscriber that fell more than one delivery
272    /// slice behind). On budget/fairness refusal the sticky overflow marker is set
273    /// and the envelope dropped (memory never grows past the bound); a closed
274    /// inbox refuses without charging or marking.
275    ///
276    /// The notifier fires OUTSIDE the state lock so the publishing actor's slice
277    /// never holds the inbox lock across the scheduler enqueue.
278    pub(crate) fn admit(&self, envelope: Envelope) -> InboxAdmission {
279        // Serialize once, before the lock: the admitted byte count is the wire
280        // size (§5 denomination). The entry stores the amount actually CHARGED
281        // (0 when no budget is installed), so dequeue/close releases exactly
282        // what enqueue charged.
283        let bytes = encode_envelope(&envelope).len();
284        let notifier = {
285            let Ok(mut state) = self.state.lock() else {
286                // A poisoned inbox lock is terminal for this subscription; treat it
287                // as a shed rather than silently dropping into a dead inbox.
288                self.overflowed.store(true, Ordering::Release);
289                return InboxAdmission::BudgetExceeded;
290            };
291            if state.closed {
292                return InboxAdmission::Closed;
293            }
294            if state.queue.len() >= state.depth_cap {
295                self.overflowed.store(true, Ordering::Release);
296                let notifier = state.notifier.clone();
297                drop(state);
298                if let Some(notifier) = notifier {
299                    notifier();
300                }
301                return InboxAdmission::FairnessTripped;
302            }
303            let charged = match state.budget.as_ref() {
304                Some(budget) => {
305                    if !budget.try_charge(bytes) {
306                        self.overflowed.store(true, Ordering::Release);
307                        let notifier = state.notifier.clone();
308                        drop(state);
309                        if let Some(notifier) = notifier {
310                            notifier();
311                        }
312                        return InboxAdmission::BudgetExceeded;
313                    }
314                    bytes
315                }
316                None => 0,
317            };
318            state.queue.push_back((envelope, charged));
319            // LEVEL-TRIGGERED, not edge-triggered: EVERY successful enqueue fires.
320            //
321            // The consumer drains a BOUNDED slice (the server's delivery pump: 32
322            // envelopes per connection slice), so an inbox more than a slice deep
323            // does NOT empty when it is serviced. Under the edge rule a subscriber
324            // in exactly that state — the normal state of anyone who has fallen
325            // behind — earned one wake for an entire burst and none afterwards,
326            // and every later envelope arrived with no wake attached to it at all.
327            // That is a lost-wake hazard on its face, and it is removed here.
328            //
329            // HONEST SCOPE, measured — this is NOT what starves a subscriber at
330            // today's bytes, and it must not be cited as if it were. A/B over 120
331            // fresh-boot iterations per arm (gate-logs/p0-55/) found the edge and
332            // level forms indistinguishable: 51.7% vs 53.3% of boots lost a
333            // subscriber to the depth-cap shed. The reason is R6 coalescing
334            // itself. N fires collapse into one mailbox drain, so turning one wake
335            // into N cannot buy the connection a single extra SLICE, and slices —
336            // not wakes — are what drain the queue. The variable that does move it
337            // is the pump's per-slice budget (32 -> 256 took the same harness to
338            // 0/120), which is a cross-connection fairness knob and not this
339            // file's to turn. See the report accompanying this lane.
340            //
341            // What firing every time costs: one non-blocking `enqueue_atom_message`
342            // per admitted envelope, which R6 coalescing collapses to one slice.
343            // An idle inbox admits nothing and so still fires nothing — the
344            // zero-cost-at-rest property is unchanged.
345            state.notifier.clone()
346        };
347        if let Some(notifier) = notifier {
348            notifier();
349        }
350        InboxAdmission::Admitted
351    }
352
353    /// Removes and returns the next envelope, releasing its CHARGED bytes back to
354    /// the shared budget (exact charge/release symmetry).
355    pub(crate) fn pop(&self) -> Option<Envelope> {
356        let (envelope, charged, budget) = {
357            let mut state = self.state.lock().ok()?;
358            let (envelope, charged) = state.queue.pop_front()?;
359            (envelope, charged, state.budget.clone())
360        };
361        // Release the charged bytes AFTER dropping the state lock so the shared
362        // budget's atomic is never touched while the inbox lock is held.
363        if let Some(budget) = budget {
364            budget.release(charged);
365        }
366        Some(envelope)
367    }
368
369    /// Non-consuming race-barrier query used after a connection arms readiness.
370    pub(crate) fn has_pending(&self) -> bool {
371        self.state.lock().is_ok_and(|state| !state.queue.is_empty())
372    }
373
374    /// Atomically closes the inbox, releasing every queued charge back to the
375    /// shared budget: under the lock it marks the inbox closed, drains all
376    /// entries, and detaches the notifier and budget; the summed release happens
377    /// outside the lock. Idempotent. Admissions after close are refused without
378    /// charging ([`InboxAdmission::Closed`]).
379    ///
380    /// This is the release-by-construction seam: explicit unsubscribe, overflow
381    /// shed, and connection teardown ALL reach it through the subscription
382    /// handle's drop (see [`SubscriptionInner::drop`]), and the inbox's own `Drop`
383    /// is the final backstop — no teardown path can strand queued bytes on the
384    /// connection-lifetime budget.
385    pub(crate) fn close(&self) {
386        let (released, budget) = {
387            let Ok(mut state) = self.state.lock() else {
388                return;
389            };
390            if state.closed {
391                return;
392            }
393            state.closed = true;
394            let released: usize = state
395                .queue
396                .drain(..)
397                .map(|(_envelope, charged)| charged)
398                .sum();
399            state.notifier = None;
400            (released, state.budget.take())
401        };
402        if let Some(budget) = budget {
403            budget.release(released);
404        }
405    }
406
407    /// Whether this subscription has been marked for shedding by an overflow.
408    pub(crate) fn is_overflowed(&self) -> bool {
409        self.overflowed.load(Ordering::Acquire)
410    }
411
412    /// Number of queued envelopes (test observability).
413    #[cfg(test)]
414    pub(crate) fn len(&self) -> usize {
415        self.state.lock().map_or(0, |state| state.queue.len())
416    }
417}
418
419impl Drop for SubscriptionInbox {
420    fn drop(&mut self) {
421        // Backstop: if no teardown path ever called `close`, release the queued
422        // charges here so the last Arc dropping can never strand budget bytes.
423        // Idempotent against an earlier close (the closed marker short-circuits).
424        self.close();
425    }
426}
427
428/// A delivery predicate evaluated by the channel actor against each published
429/// envelope. `None` (no predicate) means deliver everything.
430pub(crate) type SubscriptionPredicate = Arc<dyn Fn(&Envelope) -> bool + Send + Sync>;
431
432/// Real beamr native process backing one subscription.
433///
434/// For LOCAL delivery it is an idle handler (mirroring
435/// `aion::worker::link::IdleWorkerProcess`): local envelopes travel through the
436/// shared [`SubscriberInbox`] the channel actor writes and
437/// [`SubscriptionHandle::try_next`] reads. Its other job is to BE a first-class
438/// linkable, killable process whose lifetime equals the subscription's, so the
439/// channel actor detects the subscription dying via a real EXIT signal rather
440/// than by polling a weak pointer.
441///
442/// For CROSS-NODE delivery (SRV-005) it is also the landing point for a remote
443/// publish: a remote node sends a published envelope, encoded by
444/// [`crate::channel::wire::encode_envelope`], as a single beamr binary directly
445/// to this process's pid (the pid the cluster registered in the channel's
446/// distributed process group). The binary lands in this process's mailbox; the
447/// handler decodes it back into an [`Envelope`] and pushes it onto the SAME
448/// inbox a local publish would, so a subscriber observes local and remote
449/// messages identically. Non-binary wakeups (trapped `{EXIT, _, _}` signals) are
450/// drained and ignored.
451struct SubscriberProcess {
452    inbox: SubscriberInbox,
453}
454
455impl NativeHandler for SubscriberProcess {
456    fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
457        // Trapping is set authoritatively at spawn (see `SubscriptionHandle::spawn`)
458        // so it holds before the actor ever links — re-assert it here defensively
459        // for any future restart of this handler.
460        ctx.set_trap_exit(true);
461        // Drain every queued wakeup. A binary message is a remote envelope frame
462        // (SRV-005) to decode and enqueue; everything else (e.g. a trapped
463        // `{EXIT, _, _}` tuple from a crashed actor this subscriber outlives) is
464        // ignored. Death is driven only by an explicit `terminate_process` on
465        // unsubscribe/handle drop.
466        while let Some(message) = ctx.recv() {
467            if let Some(binary) = BinaryRef::new(message) {
468                self.accept_remote_frame(binary.as_bytes());
469            }
470        }
471        NativeOutcome::Wait
472    }
473}
474
475impl SubscriberProcess {
476    /// Decode a remote envelope frame and push it onto the inbox. A frame that
477    /// fails to decode is dropped: a corrupt cross-node payload must never crash
478    /// the subscriber or stall delivery of well-formed messages.
479    fn accept_remote_frame(&self, bytes: &[u8]) {
480        let Ok(envelope) = decode_envelope(bytes) else {
481            return;
482        };
483        // R3: the remote-delivery leg fires the same wake notifier and obeys the
484        // same §5 byte budget as the local leg. An overflow marks the subscription
485        // for shedding (inside `admit`); the frame is dropped rather than growing
486        // server memory.
487        self.inbox.admit(envelope);
488    }
489}
490
491/// The actor-side record of one subscriber: the inbox to deliver into and the
492/// optional predicate to gate delivery. Held by the channel actor INSIDE its
493/// process, keyed by the subscriber process's pid.
494pub(crate) struct SubscriberRegistration {
495    pid: u64,
496    inbox: SubscriberInbox,
497    predicate: Option<SubscriptionPredicate>,
498}
499
500impl SubscriberRegistration {
501    pub(crate) const fn pid(&self) -> u64 {
502        self.pid
503    }
504
505    /// Delivers `envelope` to this subscriber when its predicate accepts it (or
506    /// it has no predicate). Returns `true` when the envelope was pushed onto
507    /// the inbox, `false` when a predicate filtered it out, the inbox refused it
508    /// (§5 overflow — the subscription is then marked for shedding), or the inbox
509    /// is closed. The boolean lets the channel actor count genuine deliveries for
510    /// the delivery-ack signal.
511    pub(crate) fn deliver(&self, envelope: &Envelope) -> bool {
512        if let Some(predicate) = self.predicate.as_ref() {
513            if !predicate(envelope) {
514                return false;
515            }
516        }
517        // R3 + §5: admission charges the connection byte budget, fires the wake
518        // notifier for every admitted envelope, and — on overflow — marks the
519        // subscription for shedding (the server pump sheds it with a typed error
520        // frame). An overflowed envelope is NOT counted as a genuine delivery, so
521        // the delivery-ack signal reflects only envelopes that entered the inbox.
522        matches!(self.inbox.admit(envelope.clone()), InboxAdmission::Admitted)
523    }
524}
525
526impl std::fmt::Debug for SubscriberRegistration {
527    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        formatter
529            .debug_struct("SubscriberRegistration")
530            .field("pid", &self.pid)
531            .field("has_predicate", &self.predicate.is_some())
532            .finish_non_exhaustive()
533    }
534}
535
536/// Handle returned by channel subscriptions for receiving validated envelopes.
537///
538/// Owns the subscriber's beamr pid, the shared inbox, and a clone of the
539/// scheduler so the process can be terminated when the subscription ends. The
540/// handle is the subscription's lifetime: dropping the last clone terminates the
541/// subscriber process, whose EXIT prunes the channel actor's fan-out list.
542#[derive(Clone)]
543pub struct SubscriptionHandle {
544    inner: Arc<SubscriptionInner>,
545}
546
547struct SubscriptionInner {
548    pid: u64,
549    inbox: SubscriberInbox,
550    scheduler: Arc<Scheduler>,
551}
552
553impl SubscriptionHandle {
554    /// Spawns a real subscriber process on `scheduler` and returns the handle
555    /// plus its actor-side registration record (carrying any predicate).
556    ///
557    /// # Errors
558    /// Returns [`LiminalError::SubscriptionFailed`] when the scheduler cannot
559    /// spawn the subscriber process.
560    pub(crate) fn spawn(
561        scheduler: &Arc<Scheduler>,
562        predicate: Option<SubscriptionPredicate>,
563        install: Option<InboxInstall>,
564    ) -> Result<(Self, SubscriberRegistration), LiminalError> {
565        let inbox: SubscriberInbox = SubscriptionInbox::new();
566        // Install the §5 budget/fairness cap and the R3 wake notifier AT
567        // CONSTRUCTION — strictly before the registration is handed to the
568        // channel actor — so there is no window in which a publish can be
569        // admitted uncharged, past the depth cap, or without a wake.
570        if let Some(install) = install {
571            inbox.install_budget(install.budget, install.depth_cap);
572            if let Some(notifier) = install.notifier {
573                inbox.install_notifier(notifier);
574            }
575        }
576        let process_inbox = Arc::clone(&inbox);
577        let factory = Box::new(move || {
578            Box::new(SubscriberProcess {
579                inbox: Arc::clone(&process_inbox),
580            }) as Box<dyn NativeHandler>
581        });
582        // trap_exit is set on the process BEFORE it is published as runnable
583        // (0.16.1 `spawn_native_trap_exit` — pre-runnable by construction), so
584        // an abnormal channel-actor crash is trapped (delivered as a message
585        // the subscriber drains) instead of cascading across the link and
586        // killing the subscriber. This makes the subscriber outlive a
587        // channel-actor crash so the restarted actor can re-link to it on boot
588        // (R2/R4): the flag is in place before the process's first slice, with
589        // no post-spawn window for `set_trap_exit` to race (or return
590        // `NoCaller` against a mid-first-slice process).
591        let pid = scheduler.spawn_native_trap_exit(factory).map_err(|error| {
592            LiminalError::SubscriptionFailed {
593                message: format!("failed to spawn subscriber process: {error:?}"),
594            }
595        })?;
596        let handle = Self {
597            inner: Arc::new(SubscriptionInner {
598                pid,
599                inbox: Arc::clone(&inbox),
600                scheduler: Arc::clone(scheduler),
601            }),
602        };
603        let registration = SubscriberRegistration {
604            pid,
605            inbox,
606            predicate,
607        };
608        Ok((handle, registration))
609    }
610
611    /// Returns the beamr pid of the subscriber process this handle owns.
612    #[must_use]
613    pub(crate) fn pid(&self) -> u64 {
614        self.inner.pid
615    }
616
617    /// Attempts to receive the next delivered envelope without blocking.
618    ///
619    /// # Errors
620    ///
621    /// Returns [`LiminalError::SubscriptionFailed`] when the subscription inbox cannot be read.
622    pub fn try_next(&self) -> Result<Option<Envelope>, LiminalError> {
623        // Dequeue releases the envelope's admitted bytes back to the shared
624        // connection budget (§5 charge/release symmetry).
625        Ok(self.inner.inbox.pop())
626    }
627
628    /// Whether an envelope is available without consuming it.
629    #[must_use]
630    pub fn has_pending(&self) -> bool {
631        self.inner.inbox.has_pending()
632    }
633
634    /// Whether an overflow has marked this subscription for shedding (§5).
635    #[must_use]
636    pub fn is_overflowed(&self) -> bool {
637        self.inner.inbox.is_overflowed()
638    }
639}
640
641impl std::fmt::Debug for SubscriptionHandle {
642    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643        formatter
644            .debug_struct("SubscriptionHandle")
645            .field("pid", &self.inner.pid)
646            .finish_non_exhaustive()
647    }
648}
649
650impl Drop for SubscriptionInner {
651    fn drop(&mut self) {
652        // Close the inbox FIRST: atomically mark it closed, drain queued entries,
653        // and release every charged byte back to the shared connection budget
654        // (§5 — queued bytes must never be stranded on the connection-lifetime
655        // budget by unsubscribe, shed, or teardown; all of them funnel through
656        // this drop). Post-close deliveries from the channel actor (whose EXIT
657        // prune below is asynchronous) are refused without charging.
658        self.inbox.close();
659        // Terminating the subscriber process fires the bidirectional link to the
660        // channel actor, which traps the EXIT and removes this subscriber from
661        // its fan-out list. This is the real-beamr unsubscribe-on-drop path.
662        self.scheduler
663            .terminate_process(self.pid, ExitReason::Normal);
664    }
665}
666
667/// WR-9b: the REAL [`SubscriberProcess`] running on beamr's cooperative
668/// (single-threaded / wasm) [`beamr::scheduler::WasmScheduler`].
669///
670/// This proves the production subscriber handler — the same `NativeHandler` the
671/// threaded [`SubscriptionHandle::spawn`] spawns — runs unchanged on the
672/// cooperative scheduler that a browser host drives. There is no toy stand-in:
673/// the test spawns the genuine [`SubscriberProcess`], delivers a genuine
674/// [`crate::channel::wire::encode_envelope`] frame as a real beamr binary, pumps
675/// cooperative `run_until_idle` turns, and asserts the envelope is decoded by the
676/// handler's own `accept_remote_frame` path and lands in the shared inbox a
677/// [`SubscriptionHandle::try_next`] would read.
678///
679/// The handler runs cooperatively AS-IS: its `handle` only touches
680/// platform-neutral [`NativeContext`] capabilities (`set_trap_exit`, `recv`),
681/// [`BinaryRef`], and [`decode_envelope`] — none of which reach for threads,
682/// tokio, sockets, or a `SharedState`. The only wiring the smoke supplies is the
683/// cooperative driver (spawn + owned-binary delivery + turn pump), exactly the
684/// host-side seam the threaded `SubscriptionHandle`/channel-actor provide on
685/// native.
686#[cfg(test)]
687#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
688mod cooperative_smoke {
689    use std::cell::RefCell;
690    use std::rc::Rc;
691    use std::sync::Arc;
692
693    use beamr::atom::AtomTable;
694    use beamr::ets::copy_term_to_ets;
695    use beamr::module::ModuleRegistry;
696    use beamr::native::BifRegistryImpl;
697    use beamr::process::heap::Heap;
698    use beamr::scheduler::WasmScheduler;
699    use beamr::term::shared_binary::{SharedBinary, write_proc_bin};
700
701    use super::{SubscriberInbox, SubscriberProcess, SubscriptionInbox};
702    use crate::channel::SchemaId;
703    use crate::channel::wire::encode_envelope;
704    use crate::envelope::{Envelope, PublisherId};
705
706    /// Build a cooperative scheduler the way a wasm host holds it (single
707    /// `Rc<RefCell<…>>` on one thread).
708    fn cooperative_scheduler() -> Rc<RefCell<WasmScheduler>> {
709        let atom_table = Arc::new(AtomTable::with_common_atoms());
710        let modules = Arc::new(ModuleRegistry::new());
711        let bifs = Arc::new(BifRegistryImpl::new());
712        Rc::new(RefCell::new(WasmScheduler::new(atom_table, modules, bifs)))
713    }
714
715    /// Encode `envelope` into the production wire frame and wrap it as a
716    /// heap-independent beamr binary term ready for `send_owned`, mirroring how a
717    /// remote node hands a published frame to a subscriber pid (SRV-005).
718    fn frame_as_owned_binary(envelope: &Envelope) -> beamr::ets::OwnedTerm {
719        let bytes = encode_envelope(envelope);
720        let shared = SharedBinary::new(bytes);
721        // A ProcBin reference needs three heap words; copy it into ETS-owned
722        // memory so the scratch heap can be dropped before delivery.
723        let mut scratch = Heap::new(8);
724        let words = scratch
725            .alloc_slice(3)
726            .expect("scratch heap holds a proc-bin reference");
727        let term = write_proc_bin(words, &shared).expect("proc-bin term writes");
728        copy_term_to_ets(term).expect("frame copies into an owned binary")
729    }
730
731    fn sample_envelope() -> Envelope {
732        // A whole-millisecond timestamp so the round-trip through the wire codec
733        // (which carries millisecond resolution, see `channel::wire`) is exact;
734        // `Utc::now()` sub-millisecond precision would otherwise be truncated on
735        // decode and is irrelevant to what this smoke proves.
736        let timestamp = chrono::TimeZone::timestamp_millis_opt(&chrono::Utc, 1_700_000_000_123)
737            .single()
738            .expect("valid fixed millisecond timestamp");
739        Envelope::with_timestamp(
740            b"{\"value\":42}".to_vec(),
741            None,
742            SchemaId::new(),
743            PublisherId::from("publisher-cooperative"),
744            timestamp,
745        )
746    }
747
748    #[test]
749    fn real_subscriber_process_delivers_a_published_envelope_cooperatively() {
750        let scheduler = cooperative_scheduler();
751
752        // The shared inbox the subscriber pushes decoded envelopes onto — the
753        // exact channel the threaded `SubscriptionHandle::try_next` reads.
754        let inbox: SubscriberInbox = SubscriptionInbox::new();
755        let process_inbox = Arc::clone(&inbox);
756
757        // Spawn the GENUINE production subscriber handler as a first-class native
758        // process on the cooperative scheduler.
759        let pid = scheduler.borrow_mut().spawn_native_root(Box::new(move || {
760            Box::new(SubscriberProcess {
761                inbox: Arc::clone(&process_inbox),
762            }) as Box<dyn beamr::native::native_process::NativeHandler>
763        }));
764
765        // First turn: the handler runs once, asserts trap_exit, finds an empty
766        // mailbox, and parks (`Wait`). No envelope has been delivered yet.
767        scheduler.borrow_mut().run_until_idle();
768        assert_eq!(
769            inbox.len(),
770            0,
771            "no envelope is delivered before one is published"
772        );
773
774        // Publish: deliver a real encoded frame as a beamr binary straight to the
775        // subscriber pid, exactly as a remote publish lands (SRV-005). This wakes
776        // the parked process.
777        let published = sample_envelope();
778        let frame = frame_as_owned_binary(&published);
779        scheduler
780            .borrow_mut()
781            .send_owned(pid, &frame)
782            .expect("frame is delivered to the live subscriber pid");
783
784        // Pump turns: the woken handler drains the binary, decodes it through its
785        // own `accept_remote_frame` path, and pushes the envelope onto the inbox.
786        let mut delivered = None;
787        for _ in 0..8 {
788            scheduler.borrow_mut().run_until_idle();
789            let next = inbox.pop();
790            if let Some(envelope) = next {
791                delivered = Some(envelope);
792                break;
793            }
794        }
795
796        assert_eq!(
797            delivered.as_ref(),
798            Some(&published),
799            "the real subscriber decoded and delivered the published envelope"
800        );
801    }
802}
803
804/// R3 (§1.2(2)) + §5 inbox-bounding library core: the notifier fires on every
805/// admitted envelope; the shared byte budget is spent across ALL a
806/// connection's inboxes; overflow sheds the offending subscription; the per-inbox
807/// fairness trip stops one inbox starving its siblings; and charge/release is
808/// exact. These exercise [`SubscriptionInbox`]/[`ConnectionInboxBudget`] directly,
809/// with no scheduler — the server-side wake wiring and shed are tested there.
810#[cfg(test)]
811#[allow(clippy::expect_used)]
812mod inbox_bounding {
813    use std::sync::Arc;
814    use std::sync::atomic::{AtomicUsize, Ordering};
815
816    use super::{ConnectionInboxBudget, InboxAdmission, SubscriptionInbox};
817    use crate::channel::SchemaId;
818    use crate::channel::wire::encode_envelope;
819    use crate::envelope::{Envelope, PublisherId};
820
821    fn envelope(payload: &[u8]) -> Envelope {
822        Envelope::new(
823            payload.to_vec(),
824            None,
825            SchemaId::new(),
826            PublisherId::from("inbox-bounding-test"),
827        )
828    }
829
830    fn admitted_bytes(env: &Envelope) -> usize {
831        encode_envelope(env).len()
832    }
833
834    /// The level-triggered wake contract (P0 #55). This test previously asserted
835    /// the edge-triggered form — that a second admit into a non-empty inbox does
836    /// NOT re-fire — which is precisely the starvation the fix removes: the
837    /// consumer drains a bounded slice, so a non-empty inbox is the normal state
838    /// of a subscriber that has fallen behind, and withholding its wake is what
839    /// ratchets it to the depth cap and a permanent shed.
840    #[test]
841    fn notifier_fires_for_every_admitted_envelope() {
842        let inbox = SubscriptionInbox::new();
843        let fires = Arc::new(AtomicUsize::new(0));
844        let counter = Arc::clone(&fires);
845        inbox.install_notifier(Arc::new(move || {
846            counter.fetch_add(1, Ordering::Relaxed);
847        }));
848
849        // First admit into an empty inbox fires.
850        assert_eq!(inbox.admit(envelope(b"a")), InboxAdmission::Admitted);
851        assert_eq!(fires.load(Ordering::Relaxed), 1, "the first admit fires");
852
853        // A second admit into a STILL-NON-EMPTY inbox fires again: the consumer
854        // may not have reached this envelope's slice, and R6 coalescing means a
855        // redundant marker costs one mailbox atom, never a second slice of work.
856        assert_eq!(inbox.admit(envelope(b"b")), InboxAdmission::Admitted);
857        assert_eq!(
858            fires.load(Ordering::Relaxed),
859            2,
860            "an admit into a non-empty inbox still fires"
861        );
862
863        // Drain to empty, then admit again: still exactly one fire per admit.
864        assert!(inbox.pop().is_some());
865        assert!(inbox.pop().is_some());
866        assert_eq!(inbox.admit(envelope(b"c")), InboxAdmission::Admitted);
867        assert_eq!(
868            fires.load(Ordering::Relaxed),
869            3,
870            "one fire per admitted envelope, whatever the queue depth was"
871        );
872    }
873
874    #[test]
875    fn shared_budget_is_spent_across_all_a_connections_inboxes() {
876        let one = envelope(b"payload-one");
877        let two = envelope(b"payload-two");
878        // A budget large enough for exactly ONE of the two envelopes.
879        let cap = admitted_bytes(&one);
880        let budget = ConnectionInboxBudget::new(cap);
881
882        let inbox_a = SubscriptionInbox::new();
883        let inbox_b = SubscriptionInbox::new();
884        inbox_a.install_budget(Arc::clone(&budget), usize::MAX);
885        inbox_b.install_budget(Arc::clone(&budget), usize::MAX);
886
887        // Inbox A admits its envelope, consuming the whole shared budget.
888        assert_eq!(inbox_a.admit(one), InboxAdmission::Admitted);
889        assert_eq!(budget.used(), cap, "the shared budget is now fully spent");
890
891        // Inbox B — a SIBLING subscription — is refused: the budget is connection
892        // scoped, not per-inbox, so A's fill denies B.
893        assert_eq!(inbox_b.admit(two), InboxAdmission::BudgetExceeded);
894        assert!(
895            inbox_b.is_overflowed(),
896            "the sibling that overflowed the shared budget is shed"
897        );
898        assert!(!inbox_a.is_overflowed(), "the inbox that fit is not shed");
899
900        // Draining A releases its bytes back to the SHARED budget, so B could then
901        // admit (charge/release symmetry across siblings).
902        assert!(inbox_a.pop().is_some());
903        assert_eq!(
904            budget.used(),
905            0,
906            "release returns bytes to the shared budget"
907        );
908    }
909
910    #[test]
911    fn overflow_sheds_and_does_not_grow_memory() {
912        let env = envelope(b"x");
913        let budget = ConnectionInboxBudget::new(admitted_bytes(&env)); // room for one
914        let inbox = SubscriptionInbox::new();
915        inbox.install_budget(budget, usize::MAX);
916
917        assert_eq!(inbox.admit(env.clone()), InboxAdmission::Admitted);
918        // The next admit overflows: refused, marked for shedding, and NOT queued —
919        // the queue length does not grow past the bound.
920        assert_eq!(inbox.admit(env), InboxAdmission::BudgetExceeded);
921        assert!(inbox.is_overflowed());
922        assert_eq!(
923            inbox.len(),
924            1,
925            "the overflowed envelope is dropped, not queued"
926        );
927    }
928
929    #[test]
930    fn per_inbox_fairness_trip_stops_one_inbox_starving_siblings() {
931        // A huge byte budget so the FAIRNESS count — not the budget — is the trip.
932        let budget = ConnectionInboxBudget::new(usize::MAX);
933        let inbox = SubscriptionInbox::new();
934        inbox.install_budget(budget, 2); // depth cap of 2 envelopes
935
936        assert_eq!(inbox.admit(envelope(b"1")), InboxAdmission::Admitted);
937        assert_eq!(inbox.admit(envelope(b"2")), InboxAdmission::Admitted);
938        // The third trips the fairness cap even though bytes are available.
939        assert_eq!(inbox.admit(envelope(b"3")), InboxAdmission::FairnessTripped);
940        assert!(inbox.is_overflowed());
941        assert_eq!(
942            inbox.len(),
943            2,
944            "the fairness trip holds the inbox at its cap"
945        );
946    }
947
948    #[test]
949    fn charge_and_release_are_exact() {
950        let budget = ConnectionInboxBudget::new(1024 * 1024);
951        let inbox = SubscriptionInbox::new();
952        inbox.install_budget(Arc::clone(&budget), usize::MAX);
953
954        let a = envelope(b"first-envelope");
955        let b = envelope(b"second-longer-envelope-payload");
956        let charge = admitted_bytes(&a) + admitted_bytes(&b);
957        assert_eq!(inbox.admit(a), InboxAdmission::Admitted);
958        assert_eq!(inbox.admit(b), InboxAdmission::Admitted);
959        assert_eq!(budget.used(), charge, "used == sum of admitted bytes");
960
961        assert!(inbox.pop().is_some());
962        assert!(inbox.pop().is_some());
963        assert_eq!(
964            budget.used(),
965            0,
966            "every admitted byte is released on dequeue — exact symmetry"
967        );
968    }
969
970    /// Review round 1 item 4: closing an inbox with a QUEUED backlog (the shed
971    /// shape — an overflowed inbox is near-full by construction) releases every
972    /// charged byte back to the shared budget, so a sibling subscription can
973    /// admit again. Without the close-release, one shed strands its whole share
974    /// of the 4 MiB budget forever.
975    #[test]
976    fn close_releases_queued_charges_so_siblings_recover() {
977        // Budget sized so inbox A can queue a 256-envelope backlog (the §5
978        // fairness-cap depth) and exhaust the shared budget doing it.
979        let one = envelope(b"backlog-envelope-payload");
980        let unit = admitted_bytes(&one);
981        let budget = ConnectionInboxBudget::new(unit * 256);
982
983        let inbox_a = SubscriptionInbox::new();
984        let inbox_b = SubscriptionInbox::new();
985        inbox_a.install_budget(Arc::clone(&budget), usize::MAX);
986        inbox_b.install_budget(Arc::clone(&budget), usize::MAX);
987
988        // Queue the full 256-envelope backlog on A, consuming the whole budget.
989        for _ in 0..256 {
990            assert_eq!(inbox_a.admit(one.clone()), InboxAdmission::Admitted);
991        }
992        assert_eq!(budget.used(), unit * 256, "the backlog holds the budget");
993        // The sibling is starved (the shed trigger condition).
994        assert_eq!(inbox_b.admit(one.clone()), InboxAdmission::BudgetExceeded);
995
996        // Shed/unsubscribe/teardown all funnel through close: EVERY queued charge
997        // returns to the shared budget in one atomic close.
998        inbox_a.close();
999        assert_eq!(
1000            budget.used(),
1001            0,
1002            "close releases the entire queued backlog back to the shared budget"
1003        );
1004        // The sibling recovers: it can admit again.
1005        assert_eq!(
1006            inbox_b.admit(one),
1007            InboxAdmission::Admitted,
1008            "a sibling admits again after the other inbox is shed"
1009        );
1010    }
1011
1012    /// Review round 1 item 4: a closed inbox refuses admissions WITHOUT charging
1013    /// the budget, so a shed subscription can never re-accumulate cost while the
1014    /// channel actor's asynchronous EXIT prune is still in flight.
1015    #[test]
1016    fn closed_inbox_refuses_without_charging() {
1017        let env = envelope(b"post-close");
1018        let budget = ConnectionInboxBudget::new(1024 * 1024);
1019        let inbox = SubscriptionInbox::new();
1020        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1021
1022        inbox.close();
1023        assert_eq!(inbox.admit(env), InboxAdmission::Closed);
1024        assert_eq!(budget.used(), 0, "a closed inbox never charges the budget");
1025        assert_eq!(inbox.len(), 0, "a closed inbox never queues");
1026    }
1027
1028    /// Review round 1 item 4: the `Drop` backstop — if no teardown path ever
1029    /// called `close`, the last handle dropping still releases the queued charges
1030    /// (release-by-construction: a release that cannot be omitted).
1031    #[test]
1032    fn drop_backstop_releases_queued_charges() {
1033        let env = envelope(b"dropped-while-queued");
1034        let unit = admitted_bytes(&env);
1035        let budget = ConnectionInboxBudget::new(1024 * 1024);
1036        {
1037            let inbox = SubscriptionInbox::new();
1038            inbox.install_budget(Arc::clone(&budget), usize::MAX);
1039            assert_eq!(inbox.admit(env.clone()), InboxAdmission::Admitted);
1040            assert_eq!(inbox.admit(env), InboxAdmission::Admitted);
1041            assert_eq!(budget.used(), unit * 2);
1042            // No close() call: the Arc drops here.
1043        }
1044        assert_eq!(
1045            budget.used(),
1046            0,
1047            "dropping the last inbox handle releases every queued charge"
1048        );
1049    }
1050
1051    /// Review round 1 item 4: close is idempotent, and pop-after-close finds
1052    /// nothing (the queue was drained into the release).
1053    #[test]
1054    fn close_is_idempotent_and_drains_the_queue() {
1055        let env = envelope(b"x");
1056        let budget = ConnectionInboxBudget::new(1024 * 1024);
1057        let inbox = SubscriptionInbox::new();
1058        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1059        assert_eq!(inbox.admit(env), InboxAdmission::Admitted);
1060
1061        inbox.close();
1062        inbox.close(); // second close is a no-op, not a double release
1063        assert_eq!(budget.used(), 0);
1064        assert!(inbox.pop().is_none(), "a closed inbox holds nothing");
1065    }
1066
1067    /// Review round 1 item 5 (charge ownership): an envelope admitted BEFORE the
1068    /// budget was installed carries a charge of 0 — its dequeue releases exactly
1069    /// 0 against the later-installed budget, never bytes it did not charge. The
1070    /// production subscribe path installs the budget at inbox construction so
1071    /// this window is structurally closed; this pins the defensive invariant
1072    /// that makes release byte-identical to charge on EVERY entry regardless.
1073    #[test]
1074    fn per_entry_charge_ownership_survives_budget_install() {
1075        let uncharged = envelope(b"admitted-before-budget-install");
1076        let charged = envelope(b"admitted-after-budget-install");
1077        let inbox = SubscriptionInbox::new();
1078
1079        // Admitted with no budget installed: charge ownership 0.
1080        assert_eq!(inbox.admit(uncharged), InboxAdmission::Admitted);
1081
1082        let budget = ConnectionInboxBudget::new(1024 * 1024);
1083        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1084        let unit = admitted_bytes(&charged);
1085        assert_eq!(inbox.admit(charged), InboxAdmission::Admitted);
1086        assert_eq!(budget.used(), unit, "only the post-install entry charged");
1087
1088        // Popping the uncharged entry releases exactly 0 — the budget cannot
1089        // under-count (over-admitting past the signed 4 MiB) by releasing bytes
1090        // that were never charged.
1091        assert!(inbox.pop().is_some());
1092        assert_eq!(budget.used(), unit, "the uncharged entry released nothing");
1093        assert!(inbox.pop().is_some());
1094        assert_eq!(
1095            budget.used(),
1096            0,
1097            "the charged entry released its exact charge"
1098        );
1099    }
1100
1101    /// Review round 1 item 5 (install recheck): installing a notifier onto an
1102    /// ALREADY-NON-EMPTY inbox fires it exactly once — those envelopes were
1103    /// admitted while there was no notifier to fire, so the install regenerates
1104    /// their wake and one can never be lost to install ordering. (The production
1105    /// subscribe path installs at construction, when the queue is guaranteed
1106    /// empty; this pins the defensive invariant.)
1107    #[test]
1108    fn notifier_install_onto_non_empty_inbox_fires_once() {
1109        let inbox = SubscriptionInbox::new();
1110        assert_eq!(
1111            inbox.admit(envelope(b"pre-install")),
1112            InboxAdmission::Admitted
1113        );
1114
1115        let fires = Arc::new(AtomicUsize::new(0));
1116        let counter = Arc::clone(&fires);
1117        inbox.install_notifier(Arc::new(move || {
1118            counter.fetch_add(1, Ordering::Relaxed);
1119        }));
1120        assert_eq!(
1121            fires.load(Ordering::Relaxed),
1122            1,
1123            "install onto a non-empty inbox regenerates exactly one wake"
1124        );
1125
1126        // The install is a ONE-OFF regeneration, not an extra fire per admit: a
1127        // subsequent admit adds exactly its own one fire.
1128        assert_eq!(inbox.admit(envelope(b"second")), InboxAdmission::Admitted);
1129        assert_eq!(fires.load(Ordering::Relaxed), 2);
1130    }
1131}