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, OnceLock};
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::admission::{default_capacity, defer_after_append};
30use crate::channel::schema::SchemaId;
31use crate::channel::wire::{decode_envelope, encode_envelope};
32use crate::durability::bridge::block_on;
33use crate::durability::{DurableStore, MessageEnvelope, replay_range};
34use crate::envelope::{Envelope, PublisherId};
35use crate::error::LiminalError;
36use crate::pressure::{CapacityError, CapacityTracker, ConsumerCapacity, PressureSignal};
37
38/// A shared, cloneable in-memory inbox a subscriber receives delivered envelopes
39/// on. See [`SubscriptionInbox`].
40pub(crate) type SubscriberInbox = Arc<SubscriptionInbox>;
41
42/// A wake callback fired on EVERY envelope admitted to the inbox (R3, §1.2(2)).
43///
44/// The server installs one that fires the CONNECTION scheduler's `READY` marker,
45/// so a publish into a parked connection's inbox wakes it. It is called from the
46/// PUBLISHING actor's slice (the channel actor for local delivery, the subscriber
47/// process for a remote frame), so it must be cheap and non-blocking — a single
48/// `enqueue_atom_message`. `None` (no notifier installed) is the standalone
49/// library / test case: nothing to wake, delivery still lands in the inbox.
50pub type InboxNotifier = Arc<dyn Fn() + Send + Sync>;
51
52/// One shared inbox-byte budget per connection (§5).
53///
54/// Spent across ALL that connection's subscription inboxes. The accounting unit is
55/// serialized envelope bytes AS ADMITTED — charged at enqueue, released at dequeue
56/// — so the signed 4 MiB product is exact and envelope-size-independent, not a
57/// per-inbox count bounding a variable the design does not control.
58#[derive(Debug)]
59pub struct ConnectionInboxBudget {
60    used: AtomicUsize,
61    cap: usize,
62}
63
64impl ConnectionInboxBudget {
65    /// Creates a shared budget with `cap` bytes of headroom across all the
66    /// connection's inboxes.
67    #[must_use]
68    pub fn new(cap: usize) -> Arc<Self> {
69        Arc::new(Self {
70            used: AtomicUsize::new(0),
71            cap,
72        })
73    }
74
75    /// Attempts to charge `bytes`. Returns `true` and reserves the bytes when they
76    /// fit within the remaining budget, `false` (reserving nothing) on overflow. A
77    /// CAS loop keeps the reservation exact under concurrent charges from several
78    /// inboxes — no transient over-charge is ever observable.
79    fn try_charge(&self, bytes: usize) -> bool {
80        let mut current = self.used.load(Ordering::Acquire);
81        loop {
82            let Some(projected) = current.checked_add(bytes) else {
83                return false;
84            };
85            if projected > self.cap {
86                return false;
87            }
88            match self.used.compare_exchange_weak(
89                current,
90                projected,
91                Ordering::AcqRel,
92                Ordering::Acquire,
93            ) {
94                Ok(_) => return true,
95                Err(observed) => current = observed,
96            }
97        }
98    }
99
100    /// Releases `bytes` previously charged (at dequeue). Saturating so a double
101    /// release can never wrap the counter below zero.
102    fn release(&self, bytes: usize) {
103        let mut current = self.used.load(Ordering::Acquire);
104        loop {
105            let next = current.saturating_sub(bytes);
106            match self.used.compare_exchange_weak(
107                current,
108                next,
109                Ordering::AcqRel,
110                Ordering::Acquire,
111            ) {
112                Ok(_) => return,
113                Err(observed) => current = observed,
114            }
115        }
116    }
117
118    /// Bytes currently reserved across the connection's inboxes.
119    #[cfg(test)]
120    pub(crate) fn used(&self) -> usize {
121        self.used.load(Ordering::Acquire)
122    }
123}
124
125/// Everything a server connection installs onto a subscription's inbox.
126///
127/// Carries the shared §5 byte budget, the per-inbox fairness cap, and the R3
128/// wake notifier. Passed INTO the subscribe call so the installation happens at
129/// inbox construction — strictly BEFORE the registration is published to the
130/// channel actor — closing the pre-install window in which envelopes could be
131/// admitted uncharged or without a wake.
132pub struct InboxInstall {
133    /// Shared per-connection byte budget (§5).
134    pub budget: Arc<ConnectionInboxBudget>,
135    /// Per-inbox envelope-count fairness trip (§5).
136    pub depth_cap: usize,
137    /// R3 wake notifier fired on every envelope admitted to the inbox. `None`
138    /// when the caller has no waker (scheduler-free unit tests).
139    pub notifier: Option<InboxNotifier>,
140    /// A1 §2: the consumer's declared capacity — the in-flight window the
141    /// `Subscribe` frame's `max_in_flight` carries, paired with the bus-policy
142    /// buffer band. `None` keeps
143    /// [`crate::channel::admission::default_capacity`], so an inbox is bounded
144    /// whether or not the caller declared anything: there is no opt-out.
145    ///
146    /// Declared HERE rather than through a separate `subscribe_with_capacity`
147    /// because this install is the seam that was deliberately unified to make
148    /// installation happen at inbox construction, strictly before the
149    /// registration reaches the actor. A second subscribe entry point would
150    /// fork that ordering guarantee.
151    pub capacity: Option<ConsumerCapacity>,
152}
153
154impl std::fmt::Debug for InboxInstall {
155    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        formatter
157            .debug_struct("InboxInstall")
158            .field("depth_cap", &self.depth_cap)
159            .field("has_notifier", &self.notifier.is_some())
160            .field("capacity", &self.capacity)
161            .finish_non_exhaustive()
162    }
163}
164
165/// Mutable inbox state guarded by one lock: the queued envelopes (each carrying
166/// the exact bytes CHARGED for it, so release is symmetric with charge), the
167/// installed shared budget and per-inbox fairness cap, the wake notifier, and
168/// the closed marker.
169struct InboxState {
170    /// Each entry is `(envelope, charged_bytes)` — the amount actually charged to
171    /// the shared budget at enqueue (0 when no budget was installed at admit
172    /// time), released verbatim at dequeue/close. Storing the CHARGE, not the
173    /// size, makes release byte-identical to charge on every entry even across a
174    /// budget install, so the budget can never under- or over-release.
175    queue: VecDeque<(Envelope, usize)>,
176    /// Shared per-connection byte budget (§5). `None` = unbounded (standalone
177    /// library use / tests), preserving the pre-bounding behaviour exactly.
178    budget: Option<Arc<ConnectionInboxBudget>>,
179    /// Per-inbox envelope-count secondary fairness trip (§5). `usize::MAX` = off;
180    /// stops one subscription starving its siblings inside the shared byte budget.
181    depth_cap: usize,
182    /// Wake callback (R3). `None` until the connection installs one.
183    notifier: Option<InboxNotifier>,
184    /// Terminal marker set by [`SubscriptionInbox::close`]: admissions are refused
185    /// WITHOUT charging, and all queued charges have been released. Closing is the
186    /// release-by-construction seam — every teardown path (explicit unsubscribe,
187    /// overflow shed, connection teardown, and the `Drop` backstop) funnels
188    /// through it, so queued bytes can never be stranded on the connection-lifetime
189    /// budget.
190    closed: bool,
191    /// A1 §2: the consumer's declared capacity. The two bands are DERIVED from
192    /// `queue.len()` against it on every admission — never stored, never
193    /// mutated — so `queue.len() == in_flight_band + buffered_band` holds by
194    /// construction and no counter can drift or underflow.
195    capacity: ConsumerCapacity,
196    /// A1 §4: this durable subscriber missed a live push and is converging via
197    /// replay. While set, EVERY live push is shed, so the host-side refill owns
198    /// the queue and in-order delivery is preserved. Cleared only when a refill
199    /// read reaches the log head with no shed racing it.
200    ///
201    /// Never set on an ephemeral channel: there is no log to catch up from, so
202    /// an ephemeral overflow is a plain per-message Reject and nothing more.
203    lagging: bool,
204    /// Bumped on every live push shed while `lagging`. The refill loop reads it
205    /// before a head read and clears `lagging` only if it is unchanged
206    /// afterwards, so a push shed during the read window cannot be lost to a
207    /// "caught up" verdict taken before it arrived.
208    shed_generation: u64,
209    /// A1 §4: the next durable sequence this subscriber has not been offered.
210    /// Advanced by every live push that was queued (or predicate-filtered) and
211    /// by every refilled entry; frozen the moment a shed sets `lagging`, which
212    /// is what makes the frozen value the exact start of the missed range.
213    ///
214    /// MONOTONE (round 2). Advancing is `max(current, position + 1)`, never a
215    /// bare assignment. Under the ordered fan-out this is the same number every
216    /// time; if that ordering is ever broken again, monotonicity is what stops a
217    /// low-positioned push dragging the cursor back over messages this
218    /// subscriber already holds and sending the next refill to re-offer them.
219    next_replay_seq: u64,
220    /// A1 §4 exactly-once: one past the highest durable sequence the REPLAY
221    /// DOOR has offered to (or filtered past for) this subscriber. The
222    /// comparand of the live-push suppression guard, and advanced by nothing
223    /// else — not by a live push, not by the join seed.
224    ///
225    /// Split from `next_replay_seq` in round 2. The cursor answers "where does
226    /// the missed range start", which every door moves; the guard asks "did the
227    /// refill already hand this subscriber this exact position", which only the
228    /// refill can answer. Collapsing the two made a live push that arrived
229    /// behind a higher-positioned sibling look like one that had already been
230    /// delivered, and it was silently dropped.
231    replay_offered_seq: u64,
232}
233
234/// The shared subscription inbox (R3 + §5). Replaces the bare
235/// `Arc<Mutex<VecDeque<Envelope>>>`: it fires a wake notifier on every admitted
236/// envelope and enforces the connection-scoped byte budget plus the per-inbox
237/// fairness trip, shedding the offending subscription on overflow.
238pub(crate) struct SubscriptionInbox {
239    state: Mutex<InboxState>,
240    /// Sticky overflow marker: set when an admission is refused by the byte budget
241    /// or the fairness trip. The server-side delivery pump observes it and sheds
242    /// this subscription with a typed error frame, mirroring the outbound overflow
243    /// policy (a slow consumer sheds its own subscription; it cannot grow server
244    /// memory without bound). Sticky (never cleared) because a shed is terminal.
245    overflowed: AtomicBool,
246}
247
248impl std::fmt::Debug for SubscriptionInbox {
249    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        formatter
251            .debug_struct("SubscriptionInbox")
252            .field("overflowed", &self.overflowed.load(Ordering::Acquire))
253            .finish_non_exhaustive()
254    }
255}
256
257/// What an inbox did with an offered envelope.
258///
259/// Two independent regimes meet here and MUST NOT be collapsed:
260///
261/// * **A1 pacing** ([`Admitted`](Self::Admitted) / [`Deferred`](Self::Deferred)
262///   / [`Rejected`](Self::Rejected)) — per-message, non-terminal, and
263///   de-escalating. A resuming consumer walks Rejected → Deferred → Admitted as
264///   its queue drains, and NONE of these three touch the sticky overflow
265///   marker. A1's Reject sheds one message for one subscriber; it does not end
266///   the subscription.
267/// * **§5 memory safety** ([`BudgetExceeded`](Self::BudgetExceeded) /
268///   [`FairnessTripped`](Self::FairnessTripped)) — terminal. These set the
269///   sticky overflow marker and the server pump sheds the whole subscription
270///   with a typed error frame. They stay exactly as they were: the byte budget
271///   is the memory backstop, A1's counts are the pacing bound. With the
272///   defaults (128 + 1024 = 1152 envelopes against a 4096 depth cap) A1 bites
273///   first, so a §5 shed remains what it has always been — a genuine budget
274///   violation, not a slow consumer.
275///
276/// A closed inbox refuses without charging and without marking either way.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub(crate) enum InboxAdmission {
279    /// A1 Accept band: the envelope was queued with in-flight credit to spare,
280    /// and the wake notifier fired.
281    Admitted(PressureSignal),
282    /// A1 Defer band: the in-flight window is exhausted but the buffer band has
283    /// room, so the envelope WAS queued and the wake notifier fired. Defer is
284    /// purely a pacing signal to the producer — the bus already holds the
285    /// message and the consumer's next pop is its "redelivery".
286    Deferred(PressureSignal),
287    /// A1 Reject band: the buffer band is full, so the envelope is shed for
288    /// this subscriber and NOT queued. Non-terminal — the sticky overflow
289    /// marker is untouched.
290    Rejected(PressureSignal),
291    /// A1 §4 exactly-once: this durable envelope was ALREADY offered to this
292    /// subscriber — its sequence is below the REPLAY WATERMARK, so the
293    /// host-side refill read it out of the log and offered it before this live
294    /// push arrived. Suppressed here, not queued again.
295    ///
296    /// The watermark is the refill's own record, moved by no other door. A push
297    /// that merely arrives behind a higher-positioned sibling is NOT this: it
298    /// was offered by nobody, and it is queued.
299    ///
300    /// Counts as a delivery ([`Self::is_queued`]) because it genuinely was
301    /// one: the envelope entered this inbox, exactly once, by the other door.
302    /// Its signal never says Reject — a message the bus took custody of was
303    /// not shed.
304    AlreadyOffered(PressureSignal),
305    /// The shared connection byte budget (§5) had no room; the subscription is
306    /// shed. Carries the bands the envelope was shed AT, in the A1 vocabulary
307    /// — see [`Self::shed_signal`] for why that is not [`Self::signal`].
308    BudgetExceeded(PressureSignal),
309    /// The per-inbox fairness trip (§5) is full; the subscription is shed.
310    /// Carries the bands it was shed at, as [`Self::BudgetExceeded`] does.
311    FairnessTripped(PressureSignal),
312    /// The inbox was closed (unsubscribe/shed/teardown): the envelope is dropped
313    /// without charging the budget — a closed inbox can never re-accumulate cost.
314    Closed,
315}
316
317impl InboxAdmission {
318    /// Whether the envelope reached this subscriber's queue. True for the two
319    /// A1 bands that admit (Accept and Defer) and for
320    /// [`AlreadyOffered`](Self::AlreadyOffered) — where it reached the queue on
321    /// an earlier, replay-side offer of the SAME durable position — and false
322    /// for every shed and refusal. This is what the delivery-ack count means:
323    /// "the envelope entered the inbox", counted once per envelope.
324    pub(crate) const fn is_queued(&self) -> bool {
325        matches!(
326            *self,
327            Self::Admitted(_) | Self::Deferred(_) | Self::AlreadyOffered(_)
328        )
329    }
330
331    /// The A1 pressure signal this admission carries, when it took one. The
332    /// §5 refusals and a closed inbox took no pressure decision and so carry
333    /// none — they are not pacing outcomes and must never be aggregated as if
334    /// they were.
335    pub(crate) const fn signal(&self) -> Option<&PressureSignal> {
336        match *self {
337            Self::Admitted(ref signal)
338            | Self::Deferred(ref signal)
339            | Self::Rejected(ref signal)
340            | Self::AlreadyOffered(ref signal) => Some(signal),
341            Self::BudgetExceeded(_) | Self::FairnessTripped(_) | Self::Closed => None,
342        }
343    }
344
345    /// The bands a §5 refusal shed this envelope at, always a
346    /// [`PressureSignal::Reject`].
347    ///
348    /// Deliberately NOT folded into [`Self::signal`]: a §5 refusal is a
349    /// memory-safety verdict about bytes, not a pacing verdict about a slow
350    /// consumer, and it must never be reported as one. But it IS a
351    /// delivered-to-nobody outcome for its subscriber, and the producer's
352    /// aggregate answers exactly one question — did the bus take custody. With
353    /// these outcomes invisible to the aggregate, a publish EVERY subscriber
354    /// dropped resolved to the zero-subscriber `Accept` sentinel and
355    /// `is_admitted()` lied; `PressureAggregate::record_dropped` is this
356    /// accessor's only caller and the one place the two regimes meet.
357    ///
358    /// `Closed` carries none: a closed inbox belongs to a subscription that is
359    /// already gone, and counting it would turn a publish landing during a
360    /// teardown into a producer-visible Reject.
361    pub(crate) const fn shed_signal(&self) -> Option<&PressureSignal> {
362        match *self {
363            Self::BudgetExceeded(ref signal) | Self::FairnessTripped(ref signal) => Some(signal),
364            Self::Admitted(_)
365            | Self::Deferred(_)
366            | Self::Rejected(_)
367            | Self::AlreadyOffered(_)
368            | Self::Closed => None,
369        }
370    }
371}
372
373/// The A1 decision for an inbox holding `queued` envelopes under `capacity`.
374///
375/// Counts are DERIVED from the authoritative queue length (§0.1/§2) and the
376/// verdict is [`CapacityTracker::pressure_signal`] — the complete, already-
377/// tested decision model in `pressure/capacity.rs`, reused rather than
378/// reimplemented. The tracker's `record_*` mutators are never called here, so
379/// the underflow class they can produce cannot arise on this path.
380const fn derived_signal(capacity: &ConsumerCapacity, queued: usize) -> PressureSignal {
381    CapacityTracker::derived(*capacity, queued).pressure_signal()
382}
383
384/// A §5 refusal, expressed in the A1 vocabulary: the verdict is Reject (the
385/// envelope was shed for this subscriber and reached nobody) and the bands are
386/// the REAL occupancy at the moment of the refusal, split by the same
387/// [`CapacityTracker::derived`] authority the pacing decision reads. Nothing is
388/// invented and no second count is kept.
389const fn shed_signal(capacity: &ConsumerCapacity, queued: usize) -> PressureSignal {
390    let tracker = CapacityTracker::derived(*capacity, queued);
391    PressureSignal::reject(
392        tracker.current_in_flight(),
393        capacity.max_in_flight,
394        tracker.current_buffer_depth(),
395        capacity.max_buffer_depth,
396    )
397}
398
399impl SubscriptionInbox {
400    /// Creates an unbounded, notifier-less inbox — the standalone/default shape,
401    /// byte-identical to the pre-bounding behaviour. A server connection passes an
402    /// [`InboxInstall`] through subscribe so budget/cap/notifier are installed at
403    /// construction instead.
404    /// The byte budget and fairness cap are off (the standalone/default shape,
405    /// byte-identical to the pre-bounding behaviour); the A1 capacity is NOT.
406    /// Every inbox is bounded by the A1 bands from construction, with no
407    /// opt-out (§2), which is precisely what closes the unbounded-inbox hole
408    /// for a plain library `subscribe()`.
409    pub(crate) fn new() -> Arc<Self> {
410        Arc::new(Self {
411            state: Mutex::new(InboxState {
412                queue: VecDeque::new(),
413                budget: None,
414                depth_cap: usize::MAX,
415                notifier: None,
416                closed: false,
417                capacity: default_capacity(),
418                lagging: false,
419                shed_generation: 0,
420                next_replay_seq: 0,
421                replay_offered_seq: 0,
422            }),
423            overflowed: AtomicBool::new(false),
424        })
425    }
426
427    /// Installs the connection's shared byte budget and per-inbox fairness cap
428    /// (§5). Runs at inbox construction (via [`InboxInstall`]) — before the
429    /// registration is published to the channel actor — so no envelope can be
430    /// admitted uncharged.
431    pub(crate) fn install_budget(&self, budget: Arc<ConnectionInboxBudget>, depth_cap: usize) {
432        if let Ok(mut state) = self.state.lock() {
433            state.budget = Some(budget);
434            state.depth_cap = depth_cap;
435        }
436    }
437
438    /// Installs the consumer's declared A1 capacity (§2), replacing the
439    /// defaults. Runs at inbox construction on the same pre-registration
440    /// ordering guarantee as the budget install, so the first envelope the
441    /// actor can possibly deliver is already decided against the declared
442    /// window.
443    ///
444    /// # Errors
445    ///
446    /// Returns [`CapacityError::InvalidCapacity`] when a band is zero. A zero
447    /// window would make every publish a Reject, which is a configuration
448    /// fault, not a pressure decision — so it is REFUSED, aloud.
449    ///
450    /// It used to return early and say nothing, which is worse than either
451    /// answer: the inbox kept the library defaults (128 + 1024) while the
452    /// caller had every reason to believe its declared window was in force. A
453    /// declared window must install or refuse; a subscription running under a
454    /// bound nobody agreed to is invisible, and a failed subscribe is not.
455    pub(crate) fn install_capacity(&self, capacity: ConsumerCapacity) -> Result<(), CapacityError> {
456        capacity.validate()?;
457        if let Ok(mut state) = self.state.lock() {
458            state.capacity = capacity;
459        }
460        Ok(())
461    }
462
463    /// Seeds the replay cursor at the durable log head the subscription joined
464    /// at (§4). A gap this subscriber never had cannot be a gap it has to
465    /// catch up on, so a fresh subscription's missed range starts where it
466    /// started listening — not at sequence zero, which would replay the whole
467    /// history on the first shed.
468    ///
469    /// It seeds the REPLAY ORIGIN only, never the suppression watermark. A row
470    /// appended just below the head can still be fanned out after this
471    /// registration lands, and that live push is DELIVERED: the fan-out list is
472    /// the authority on who is a live subscriber, this subscription was on it,
473    /// and no door ever offered that position. Suppressing it would drop a
474    /// message the producer is told was delivered, to buy nothing — the refill
475    /// starts at the seed, so it can never re-offer the position and there is
476    /// no duplicate to prevent.
477    pub(crate) fn seed_replay_cursor(&self, head: u64) {
478        if let Ok(mut state) = self.state.lock() {
479            state.next_replay_seq = head;
480        }
481    }
482
483    /// Installs the wake notifier (R3), fired on every admitted envelope,
484    /// capturing the connection scheduler's enqueue handle (§1.2(2)).
485    ///
486    /// Defensive invariant: the install RECHECKS non-emptiness under the lock and
487    /// fires the notifier (outside the lock) when envelopes are already queued —
488    /// those envelopes were admitted while there was no notifier to fire, so
489    /// without this recheck their wake would be lost to install ordering. On the
490    /// normal construction path the queue is empty and this is a no-op.
491    pub(crate) fn install_notifier(&self, notifier: InboxNotifier) {
492        let fire = {
493            let Ok(mut state) = self.state.lock() else {
494                return;
495            };
496            let pending = !state.queue.is_empty();
497            let handle = notifier.clone();
498            state.notifier = Some(notifier);
499            pending.then_some(handle)
500        };
501        if let Some(notifier) = fire {
502            notifier();
503        }
504    }
505
506    /// Admits `envelope` under the byte budget and fairness trip, charging the
507    /// serialized bytes as admitted and firing the wake notifier for EVERY
508    /// admitted envelope (level-triggered — see the fire site below for why the
509    /// edge-triggered form starved a subscriber that fell more than one delivery
510    /// slice behind). On budget/fairness refusal the sticky overflow marker is set
511    /// and the envelope dropped (memory never grows past the bound); a closed
512    /// inbox refuses without charging or marking.
513    ///
514    /// The notifier fires OUTSIDE the state lock so the publishing actor's slice
515    /// never holds the inbox lock across the scheduler enqueue.
516    pub(crate) fn admit(&self, envelope: Envelope) -> InboxAdmission {
517        self.admit_at(envelope, None)
518    }
519
520    /// [`Self::admit`] carrying the envelope's durable sequence, when the
521    /// publishing channel has one (A1 §4).
522    ///
523    /// The position is what makes auto-catch-up possible: a queued envelope
524    /// advances the replay cursor past itself, and the first shed FREEZES the
525    /// cursor at the message it lost, so the missed range is exactly
526    /// `[next_replay_seq, head)` with no bookkeeping that could disagree with
527    /// the queue.
528    ///
529    /// That sentence assumes durable positions arrive here in ASCENDING ORDER,
530    /// and they do: the fan-out command is enqueued under the append lock that
531    /// assigned the position and the actor drains its queue in push order (see
532    /// `ChannelHandle::persist_and_enqueue`). The cursor still advances by
533    /// `max` rather than assignment, so a future reordering costs a duplicate
534    /// at worst and never a message.
535    ///
536    /// The A1 band decision is taken FIRST — ahead of the §5 depth cap and byte
537    /// budget — and never touches the sticky overflow marker, so an A1 Reject
538    /// paces one message instead of killing the subscription. It reads
539    /// `queue.len()` under the SAME lock that then pushes, so decide+push is
540    /// atomic against a concurrent [`Self::pop`] and concurrent publishers
541    /// serialise on it (the publish-time TOCTOU §2 closes).
542    pub(crate) fn admit_at(
543        &self,
544        envelope: Envelope,
545        durable_position: Option<u64>,
546    ) -> InboxAdmission {
547        self.admit_inner(envelope, durable_position, false)
548    }
549
550    /// The replay door (A1 §4): admits one entry the host-side refill read out
551    /// of the durable log, through the SAME bounded admission a live push takes.
552    ///
553    /// Returns whether the entry was queued; `false` stops the refill loop and
554    /// leaves the subscriber lagging, so the next pop retries. This is the ONLY
555    /// writer allowed to move the replay cursor while the gap is open — a live
556    /// push is shed instead, which is what preserves in-order delivery.
557    ///
558    /// It is also the only writer of `replay_offered_seq`, alongside
559    /// [`Self::note_replayed_filtered`]: this door is the one that can offer a
560    /// position ahead of that position's own live push, so this door's record
561    /// is the only honest basis for suppressing that push when it arrives.
562    pub(crate) fn admit_replayed(&self, envelope: Envelope, sequence: u64) -> bool {
563        self.admit_inner(envelope, Some(sequence), true).is_queued()
564    }
565
566    /// Steps the replay cursor past an entry the refill read but this
567    /// subscriber's predicate filtered out. Unlike [`Self::note_filtered`] this
568    /// advances WHILE lagging, because the refill owns the cursor then.
569    ///
570    /// It advances the replay watermark too: the refill has DEALT with this
571    /// position — a live push for it would be the second offer of a message
572    /// this subscription has already decided it does not want, and the guard
573    /// must suppress it exactly as it suppresses a re-offered queued one.
574    pub(crate) fn note_replayed_filtered(&self, sequence: u64) {
575        if let Ok(mut state) = self.state.lock()
576            && !state.closed
577        {
578            let next = sequence.saturating_add(1);
579            state.next_replay_seq = state.next_replay_seq.max(next);
580            state.replay_offered_seq = state.replay_offered_seq.max(next);
581        }
582    }
583
584    /// The one admission body. `replay` distinguishes the host-side refill from
585    /// a live push: only a live push is shed while lagging, and only a live
586    /// push can OPEN a gap.
587    fn admit_inner(
588        &self,
589        envelope: Envelope,
590        durable_position: Option<u64>,
591        replay: bool,
592    ) -> InboxAdmission {
593        // Serialize once, before the lock: the admitted byte count is the wire
594        // size (§5 denomination). The entry stores the amount actually CHARGED
595        // (0 when no budget is installed), so dequeue/close releases exactly
596        // what enqueue charged.
597        let bytes = encode_envelope(&envelope).len();
598        let signal;
599        let notifier = {
600            let Ok(mut state) = self.state.lock() else {
601                // A poisoned inbox lock is terminal for this subscription; treat it
602                // as a shed rather than silently dropping into a dead inbox.
603                //
604                // Zero bands, matching `capacity_bound`'s poisoned-lock reading:
605                // an inbox whose lock is poisoned admits nothing and holds no
606                // live buffer, so there is no occupancy to report. The verdict
607                // — shed, reached nobody — is the part that is certain.
608                self.overflowed.store(true, Ordering::Release);
609                return InboxAdmission::BudgetExceeded(PressureSignal::reject(0, 0, 0, 0));
610            };
611            if state.closed {
612                return InboxAdmission::Closed;
613            }
614            // A1 §4 EXACTLY ONCE, and the seam that makes it true.
615            //
616            // The durable publish path appends and THEN fans out
617            // (`ChannelHandle::publish_with_delivery`), so a row is readable
618            // from the log strictly before its live push reaches any inbox. The
619            // host-side refill reads the log. It can therefore read, offer, and
620            // step past a row whose live push has not happened yet — and then
621            // clear `lagging` legitimately, because nothing was SHED and the
622            // shed generation is unchanged. The live push then arrives at a
623            // caught-up subscriber and would be queued a SECOND time.
624            //
625            // The comparand is `replay_offered_seq`, and it is deliberately NOT
626            // the replay cursor (round 2). Only the replay door can offer a
627            // position ahead of that position's own live push, so only the
628            // replay door can create the duplicate this guard exists to
629            // suppress — and `replay_offered_seq` is advanced by the replay
630            // door alone. A live push below it was genuinely handed to this
631            // subscriber already, out of the log, and its second offer is
632            // suppressed here.
633            //
634            // WHY NOT THE CURSOR. `next_replay_seq` is advanced by every door,
635            // a live push included, and a live push moves it PAST ITSELF. Test
636            // a live push against it and any push that arrives behind a
637            // higher-positioned sibling reads as already-offered although
638            // nothing ever offered it — silent loss, counted to the producer as
639            // a delivery (`is_queued`), with nothing shed to make it
640            // recoverable. That is strictly worse than the duplicate, and it is
641            // what round 1 shipped.
642            //
643            // The publish path is now ordered — the fan-out command is enqueued
644            // under the append lock that assigned the position
645            // (`ChannelHandle::persist_and_enqueue`) — so an inversion cannot
646            // reach this line at all. This guard does not RELY on that: it is
647            // the second, independent reason the loss cannot happen, and the
648            // one that keeps the failure direction safe (a duplicate, never a
649            // loss) if the publish path is ever reordered again.
650            //
651            // Checked BEFORE the lagging shed on purpose: an already-offered
652            // envelope must not bump the shed generation, or a refill would
653            // loop chasing a gap that a delivered message opened.
654            if !replay
655                && let Some(position) = durable_position
656                && position < state.replay_offered_seq
657            {
658                // Never a Reject: the bus took custody of this envelope for
659                // this subscriber. The bands still report the real occupancy,
660                // so the producer's pacing hint stays honest.
661                let signal = defer_after_append(derived_signal(&state.capacity, state.queue.len()));
662                return InboxAdmission::AlreadyOffered(signal);
663            }
664            // A1 §4: a lagging durable subscriber sheds EVERY live push so the
665            // host-side refill can deliver the missed range in order. Its live
666            // band is closed by policy, which is reported as a full buffer band
667            // — true of the live path, and it keeps the aggregate's
668            // `Reject => delivered to nobody` exact.
669            if state.lagging && !replay {
670                state.shed_generation = state.shed_generation.wrapping_add(1);
671                let capacity = state.capacity;
672                return InboxAdmission::Rejected(PressureSignal::reject(
673                    capacity.max_in_flight,
674                    capacity.max_in_flight,
675                    capacity.max_buffer_depth,
676                    capacity.max_buffer_depth,
677                ));
678            }
679            signal = derived_signal(&state.capacity, state.queue.len());
680            if matches!(signal, PressureSignal::Reject { .. }) {
681                // Shed for THIS message only: nothing is queued, nothing is
682                // charged, and `overflowed` stays exactly as it was. The next
683                // pop moves this subscriber back into the Defer band.
684                //
685                // On a durable channel the shed OPENS a gap: the cursor is
686                // already at this sequence and is now frozen there, so the
687                // missed range starts exactly at the message that was lost. A
688                // refill Reject means the free band was already full and is not
689                // a new gap — the subscriber is lagging already.
690                if durable_position.is_some() && !replay {
691                    state.lagging = true;
692                    state.shed_generation = state.shed_generation.wrapping_add(1);
693                }
694                return InboxAdmission::Rejected(signal);
695            }
696            if state.queue.len() >= state.depth_cap {
697                self.overflowed.store(true, Ordering::Release);
698                let shed = shed_signal(&state.capacity, state.queue.len());
699                let notifier = state.notifier.clone();
700                drop(state);
701                if let Some(notifier) = notifier {
702                    notifier();
703                }
704                return InboxAdmission::FairnessTripped(shed);
705            }
706            let charged = match state.budget.as_ref() {
707                Some(budget) => {
708                    if !budget.try_charge(bytes) {
709                        self.overflowed.store(true, Ordering::Release);
710                        let shed = shed_signal(&state.capacity, state.queue.len());
711                        let notifier = state.notifier.clone();
712                        drop(state);
713                        if let Some(notifier) = notifier {
714                            notifier();
715                        }
716                        return InboxAdmission::BudgetExceeded(shed);
717                    }
718                    bytes
719                }
720                None => 0,
721            };
722            state.queue.push_back((envelope, charged));
723            // A1 §4: this envelope is now this subscriber's, so the replay
724            // cursor steps past it. Done under the SAME lock as the push, so a
725            // concurrent shed cannot interleave and leave the cursor pointing
726            // at a message that was in fact delivered.
727            //
728            // `max`, not assignment: the cursor names where the missed range
729            // STARTS, and it must never be dragged back below a position this
730            // subscriber already holds — a refill from there would re-read and
731            // re-offer delivered messages.
732            //
733            // The replay watermark moves only on the replay door's own offers.
734            // That is the whole point of it being a separate number: it is the
735            // record of what the REFILL handed over, and a live push is not
736            // evidence about that.
737            if let Some(position) = durable_position {
738                let next = position.saturating_add(1);
739                state.next_replay_seq = state.next_replay_seq.max(next);
740                if replay {
741                    state.replay_offered_seq = state.replay_offered_seq.max(next);
742                }
743            }
744            // LEVEL-TRIGGERED, not edge-triggered: EVERY successful enqueue fires.
745            //
746            // The consumer drains a BOUNDED slice (the server's delivery pump: 32
747            // envelopes per connection slice), so an inbox more than a slice deep
748            // does NOT empty when it is serviced. Under the edge rule a subscriber
749            // in exactly that state — the normal state of anyone who has fallen
750            // behind — earned one wake for an entire burst and none afterwards,
751            // and every later envelope arrived with no wake attached to it at all.
752            // That is a lost-wake hazard on its face, and it is removed here.
753            //
754            // HONEST SCOPE, measured — this is NOT what starves a subscriber at
755            // today's bytes, and it must not be cited as if it were. A/B over 120
756            // fresh-boot iterations per arm (gate-logs/p0-55/) found the edge and
757            // level forms indistinguishable: 51.7% vs 53.3% of boots lost a
758            // subscriber to the depth-cap shed. The reason is R6 coalescing
759            // itself. N fires collapse into one mailbox drain, so turning one wake
760            // into N cannot buy the connection a single extra SLICE, and slices —
761            // not wakes — are what drain the queue. The variable that does move it
762            // is the pump's per-slice budget (32 -> 256 took the same harness to
763            // 0/120), which is a cross-connection fairness knob and not this
764            // file's to turn. See the report accompanying this lane.
765            //
766            // What firing every time costs: one non-blocking `enqueue_atom_message`
767            // per admitted envelope, which R6 coalescing collapses to one slice.
768            // An idle inbox admits nothing and so still fires nothing — the
769            // zero-cost-at-rest property is unchanged.
770            state.notifier.clone()
771        };
772        if let Some(notifier) = notifier {
773            notifier();
774        }
775        if matches!(signal, PressureSignal::Defer { .. }) {
776            InboxAdmission::Deferred(signal)
777        } else {
778            InboxAdmission::Admitted(signal)
779        }
780    }
781
782    /// Steps the replay cursor past an envelope this subscriber's predicate
783    /// filtered out (A1 §2 + §4).
784    ///
785    /// A non-matching envelope contributes no backpressure and is not a gap:
786    /// leaving the cursor behind it would make every later refill re-read and
787    /// re-filter it forever. A lagging subscriber's cursor stays frozen — the
788    /// refill owns it until the gap closes.
789    pub(crate) fn note_filtered(&self, durable_position: u64) {
790        if let Ok(mut state) = self.state.lock()
791            && !state.lagging
792            && !state.closed
793        {
794            state.next_replay_seq = state
795                .next_replay_seq
796                .max(durable_position.saturating_add(1));
797        }
798    }
799
800    /// Removes and returns the next envelope, releasing its CHARGED bytes back to
801    /// the shared budget (exact charge/release symmetry).
802    pub(crate) fn pop(&self) -> Option<Envelope> {
803        let (envelope, charged, budget) = {
804            let mut state = self.state.lock().ok()?;
805            let (envelope, charged) = state.queue.pop_front()?;
806            (envelope, charged, state.budget.clone())
807        };
808        // Release the charged bytes AFTER dropping the state lock so the shared
809        // budget's atomic is never touched while the inbox lock is held.
810        if let Some(budget) = budget {
811            budget.release(charged);
812        }
813        Some(envelope)
814    }
815
816    /// Non-consuming race-barrier query used after a connection arms readiness.
817    pub(crate) fn has_pending(&self) -> bool {
818        self.state.lock().is_ok_and(|state| !state.queue.is_empty())
819    }
820
821    /// Atomically closes the inbox, releasing every queued charge back to the
822    /// shared budget: under the lock it marks the inbox closed, drains all
823    /// entries, and detaches the notifier and budget; the summed release happens
824    /// outside the lock. Idempotent. Admissions after close are refused without
825    /// charging ([`InboxAdmission::Closed`]).
826    ///
827    /// This is the release-by-construction seam: explicit unsubscribe, overflow
828    /// shed, and connection teardown ALL reach it through the subscription
829    /// handle's drop (see [`SubscriptionInner::drop`]), and the inbox's own `Drop`
830    /// is the final backstop — no teardown path can strand queued bytes on the
831    /// connection-lifetime budget.
832    pub(crate) fn close(&self) {
833        let (released, budget) = {
834            let Ok(mut state) = self.state.lock() else {
835                return;
836            };
837            if state.closed {
838                return;
839            }
840            state.closed = true;
841            let released: usize = state
842                .queue
843                .drain(..)
844                .map(|(_envelope, charged)| charged)
845                .sum();
846            state.notifier = None;
847            (released, state.budget.take())
848        };
849        if let Some(budget) = budget {
850            budget.release(released);
851        }
852    }
853
854    /// Whether this subscription has been marked for shedding by an overflow.
855    pub(crate) fn is_overflowed(&self) -> bool {
856        self.overflowed.load(Ordering::Acquire)
857    }
858
859    /// Number of queued envelopes.
860    pub(crate) fn queued_len(&self) -> usize {
861        self.state.lock().map_or(0, |state| state.queue.len())
862    }
863
864    /// Number of queued envelopes (test observability alias).
865    #[cfg(test)]
866    pub(crate) fn len(&self) -> usize {
867        self.queued_len()
868    }
869
870    /// This inbox's total A1 bound: `max_in_flight + max_buffer_depth`.
871    ///
872    /// A poisoned lock reports `0`, which contributes nothing to the durable
873    /// channel-aggregate watermark — the honest reading, since an inbox whose
874    /// lock is poisoned admits nothing and so holds no live buffer.
875    pub(crate) fn capacity_bound(&self) -> usize {
876        self.state.lock().map_or(0, |state| {
877            state
878                .capacity
879                .max_in_flight
880                .saturating_add(state.capacity.max_buffer_depth)
881        })
882    }
883
884    /// Whether this subscriber is converging on the durable log via replay
885    /// (A1 §4).
886    pub(crate) fn is_lagging(&self) -> bool {
887        self.state.lock().is_ok_and(|state| state.lagging)
888    }
889
890    /// The next refill batch to read, or `None` when none is due (A1 §4).
891    ///
892    /// `Some((generation, cursor, budget))` only when this subscriber is
893    /// lagging AND its queue has drained below the low watermark
894    /// (`max_in_flight / 2`) AND the free buffer band is non-empty. That last
895    /// conjunct is load-bearing: a zero-length read would return an empty batch
896    /// and be indistinguishable from "caught up", which would clear `lagging`
897    /// while the gap was still open.
898    ///
899    /// `budget` is the free band, so a replay batch can never blow the bound it
900    /// exists to serve (§4 "replay batches size themselves to the free buffer
901    /// band").
902    pub(crate) fn refill_plan(&self) -> Option<(u64, u64, usize)> {
903        let state = self.state.lock().ok()?;
904        if !state.lagging || state.closed {
905            return None;
906        }
907        let queued = state.queue.len();
908        let low_watermark = state.capacity.max_in_flight / 2;
909        if queued > low_watermark {
910            return None;
911        }
912        let bound = state
913            .capacity
914            .max_in_flight
915            .saturating_add(state.capacity.max_buffer_depth);
916        let budget = bound.saturating_sub(queued);
917        if budget == 0 {
918            return None;
919        }
920        Some((state.shed_generation, state.next_replay_seq, budget))
921    }
922
923    /// Declares the gap closed, unless a live push was shed since `generation`
924    /// was taken (A1 §4).
925    ///
926    /// Returns `true` when `lagging` was cleared. The generation check is what
927    /// makes the loop-until-caught-up test correct under concurrent appends: a
928    /// push shed while the head read was in flight bumps the generation, so the
929    /// caller loops instead of declaring victory over a message it never
930    /// delivered.
931    pub(crate) fn clear_lagging_if_unchanged(&self, generation: u64) -> bool {
932        let Ok(mut state) = self.state.lock() else {
933            return false;
934        };
935        if state.shed_generation != generation {
936            return false;
937        }
938        state.lagging = false;
939        true
940    }
941}
942
943impl Drop for SubscriptionInbox {
944    fn drop(&mut self) {
945        // Backstop: if no teardown path ever called `close`, release the queued
946        // charges here so the last Arc dropping can never strand budget bytes.
947        // Idempotent against an earlier close (the closed marker short-circuits).
948        self.close();
949    }
950}
951
952/// A delivery predicate evaluated by the channel actor against each published
953/// envelope. `None` (no predicate) means deliver everything.
954pub(crate) type SubscriptionPredicate = Arc<dyn Fn(&Envelope) -> bool + Send + Sync>;
955
956/// Real beamr native process backing one subscription.
957///
958/// For LOCAL delivery it is an idle handler (mirroring
959/// `aion::worker::link::IdleWorkerProcess`): local envelopes travel through the
960/// shared [`SubscriberInbox`] the channel actor writes and
961/// [`SubscriptionHandle::try_next`] reads. Its other job is to BE a first-class
962/// linkable, killable process whose lifetime equals the subscription's, so the
963/// channel actor detects the subscription dying via a real EXIT signal rather
964/// than by polling a weak pointer.
965///
966/// For CROSS-NODE delivery (SRV-005) it is also the landing point for a remote
967/// publish: a remote node sends a published envelope, encoded by
968/// [`crate::channel::wire::encode_envelope`], as a single beamr binary directly
969/// to this process's pid (the pid the cluster registered in the channel's
970/// distributed process group). The binary lands in this process's mailbox; the
971/// handler decodes it back into an [`Envelope`] and pushes it onto the SAME
972/// inbox a local publish would, so a subscriber observes local and remote
973/// messages identically. Non-binary wakeups (trapped `{EXIT, _, _}` signals) are
974/// drained and ignored.
975struct SubscriberProcess {
976    inbox: SubscriberInbox,
977}
978
979impl NativeHandler for SubscriberProcess {
980    fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
981        // Trapping is set authoritatively at spawn (see `SubscriptionHandle::spawn`)
982        // so it holds before the actor ever links — re-assert it here defensively
983        // for any future restart of this handler.
984        ctx.set_trap_exit(true);
985        // Drain every queued wakeup. A binary message is a remote envelope frame
986        // (SRV-005) to decode and enqueue; everything else (e.g. a trapped
987        // `{EXIT, _, _}` tuple from a crashed actor this subscriber outlives) is
988        // ignored. Death is driven only by an explicit `terminate_process` on
989        // unsubscribe/handle drop.
990        while let Some(message) = ctx.recv() {
991            if BinaryRef::new(message).is_some() {
992                // beamr 0.20.0 ties byte access to a `HeapBorrow` witness, and
993                // `NativeContext` exposes no borrow of the process heap, so the
994                // frame is deep-copied into ETS-owned storage and read under
995                // the copy's own witness — the one borrow-correct path a
996                // native handler has (beamr#36 tracks a direct accessor). A
997                // frame whose copy fails is dropped, same as one that fails to
998                // decode: a corrupt cross-node payload must never crash the
999                // subscriber or stall delivery of well-formed messages.
1000                if let Ok(owned) = beamr::ets::copy_term_to_ets(message)
1001                    && let Some(binary) = BinaryRef::new(owned.root())
1002                {
1003                    self.accept_remote_frame(binary.as_bytes(owned.borrow_terms()));
1004                }
1005            }
1006        }
1007        NativeOutcome::Wait
1008    }
1009}
1010
1011impl SubscriberProcess {
1012    /// Decode a remote envelope frame and push it onto the inbox. A frame that
1013    /// fails to decode is dropped: a corrupt cross-node payload must never crash
1014    /// the subscriber or stall delivery of well-formed messages.
1015    fn accept_remote_frame(&self, bytes: &[u8]) {
1016        let Ok(envelope) = decode_envelope(bytes) else {
1017            return;
1018        };
1019        // R3: the remote-delivery leg fires the same wake notifier and obeys the
1020        // same §5 byte budget as the local leg. An overflow marks the subscription
1021        // for shedding (inside `admit`); the frame is dropped rather than growing
1022        // server memory.
1023        self.inbox.admit(envelope);
1024    }
1025}
1026
1027/// The actor-side record of one subscriber: the inbox to deliver into and the
1028/// optional predicate to gate delivery. Held by the channel actor INSIDE its
1029/// process, keyed by the subscriber process's pid.
1030pub(crate) struct SubscriberRegistration {
1031    pid: u64,
1032    inbox: SubscriberInbox,
1033    predicate: Option<SubscriptionPredicate>,
1034}
1035
1036impl SubscriberRegistration {
1037    pub(crate) const fn pid(&self) -> u64 {
1038        self.pid
1039    }
1040
1041    /// Offers `envelope` to this subscriber, returning the admission outcome —
1042    /// or `None` when the subscriber's predicate filtered it out.
1043    ///
1044    /// `None` is not a refusal and is deliberately distinct from every
1045    /// [`InboxAdmission`]: A1 §2 requires the decision to run **after the
1046    /// predicate**, so a non-matching subscriber contributes no backpressure at
1047    /// all. Folding it in as a Reject would produce false Defer signals for
1048    /// producers on a heavily filtered channel.
1049    ///
1050    /// R3 + §5 are unchanged underneath: admission charges the connection byte
1051    /// budget, fires the wake notifier for every queued envelope, and on a
1052    /// §5 overflow marks the subscription for shedding. Only
1053    /// [`InboxAdmission::is_queued`] counts as a genuine delivery.
1054    pub(crate) fn deliver(
1055        &self,
1056        envelope: &Envelope,
1057        durable_position: Option<u64>,
1058    ) -> Option<InboxAdmission> {
1059        if let Some(predicate) = self.predicate.as_ref() {
1060            if !predicate(envelope) {
1061                // Not this subscriber's message, so not this subscriber's gap:
1062                // step the replay cursor past it (A1 §4).
1063                if let Some(position) = durable_position {
1064                    self.inbox.note_filtered(position);
1065                }
1066                return None;
1067            }
1068        }
1069        Some(self.inbox.admit_at(envelope.clone(), durable_position))
1070    }
1071
1072    /// This subscriber's live-buffer occupancy and its bound, for the durable
1073    /// pre-append watermark (A1 §4). Read HOST-SIDE, off the actor's command
1074    /// slice, and deliberately coarse: no predicate is evaluated.
1075    pub(crate) fn occupancy(&self) -> (usize, usize) {
1076        (self.inbox.queued_len(), self.inbox.capacity_bound())
1077    }
1078}
1079
1080impl std::fmt::Debug for SubscriberRegistration {
1081    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082        formatter
1083            .debug_struct("SubscriberRegistration")
1084            .field("pid", &self.pid)
1085            .field("has_predicate", &self.predicate.is_some())
1086            .finish_non_exhaustive()
1087    }
1088}
1089
1090/// Handle returned by channel subscriptions for receiving validated envelopes.
1091///
1092/// Owns the subscriber's beamr pid, the shared inbox, and a clone of the
1093/// scheduler so the process can be terminated when the subscription ends. The
1094/// handle is the subscription's lifetime: dropping the last clone terminates the
1095/// subscriber process, whose EXIT prunes the channel actor's fan-out list.
1096#[derive(Clone)]
1097pub struct SubscriptionHandle {
1098    inner: Arc<SubscriptionInner>,
1099}
1100
1101struct SubscriptionInner {
1102    pid: u64,
1103    inbox: SubscriberInbox,
1104    scheduler: Arc<Scheduler>,
1105    /// A1 §4: everything the host-side catch-up needs, attached by the durable
1106    /// subscribe path. `OnceLock` because it is written exactly once, before
1107    /// the handle is handed to the caller, and read on every `try_next`.
1108    /// Absent on an ephemeral channel — there is nothing to replay from.
1109    refill: OnceLock<DurableRefill>,
1110    /// Serialises concurrent refills of ONE subscription. Contended only when
1111    /// two threads drain the same handle; the loser skips (the winner is
1112    /// already filling the same queue) rather than queueing behind a store
1113    /// read.
1114    refilling: Mutex<()>,
1115}
1116
1117/// The host-side auto-catch-up source for a lagging durable subscriber
1118/// (A1 §4, graft §0.4).
1119///
1120/// Deliberately held by the SUBSCRIPTION HANDLE, not by the channel actor: the
1121/// implementability judge relocated catch-up off the actor's command slice
1122/// precisely so a blocking store read can never stall the actor for every
1123/// other subscriber on the channel. The refill runs on whichever thread is
1124/// draining this one subscription.
1125struct DurableRefill {
1126    store: Arc<dyn DurableStore>,
1127    /// The channel's durable partition stream key.
1128    stream_key: String,
1129    /// The schema id stamped on refilled envelopes: the channel's schema at
1130    /// subscribe time. The durable record stores validated payload bytes, not
1131    /// the schema that validated them, so this is the only id available and
1132    /// pretending otherwise would be an invention.
1133    schema_id: SchemaId,
1134    /// The subscription's predicate, re-applied to refilled envelopes so a
1135    /// filtered subscription catches up on exactly the messages it would have
1136    /// received live.
1137    predicate: Option<SubscriptionPredicate>,
1138}
1139
1140impl std::fmt::Debug for DurableRefill {
1141    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1142        formatter
1143            .debug_struct("DurableRefill")
1144            .field("stream_key", &self.stream_key)
1145            .field("has_predicate", &self.predicate.is_some())
1146            .finish_non_exhaustive()
1147    }
1148}
1149
1150impl SubscriptionHandle {
1151    /// Spawns a real subscriber process on `scheduler` and returns the handle
1152    /// plus its actor-side registration record (carrying any predicate).
1153    ///
1154    /// # Errors
1155    /// Returns [`LiminalError::SubscriptionFailed`] when the scheduler cannot
1156    /// spawn the subscriber process.
1157    pub(crate) fn spawn(
1158        scheduler: &Arc<Scheduler>,
1159        predicate: Option<SubscriptionPredicate>,
1160        install: Option<InboxInstall>,
1161    ) -> Result<(Self, SubscriberRegistration), LiminalError> {
1162        let inbox: SubscriberInbox = SubscriptionInbox::new();
1163        // Install the §5 budget/fairness cap and the R3 wake notifier AT
1164        // CONSTRUCTION — strictly before the registration is handed to the
1165        // channel actor — so there is no window in which a publish can be
1166        // admitted uncharged, past the depth cap, or without a wake.
1167        if let Some(install) = install {
1168            inbox.install_budget(install.budget, install.depth_cap);
1169            if let Some(capacity) = install.capacity {
1170                // REFUSES ALOUD. A window the bus cannot honour fails the
1171                // subscribe rather than leaving the caller on defaults it never
1172                // asked for and cannot observe.
1173                inbox.install_capacity(capacity).map_err(|error| {
1174                    LiminalError::SubscriptionFailed {
1175                        message: format!("declared consumer capacity is invalid: {error}"),
1176                    }
1177                })?;
1178            }
1179            if let Some(notifier) = install.notifier {
1180                inbox.install_notifier(notifier);
1181            }
1182        }
1183        let process_inbox = Arc::clone(&inbox);
1184        let factory = Box::new(move || {
1185            Box::new(SubscriberProcess {
1186                inbox: Arc::clone(&process_inbox),
1187            }) as Box<dyn NativeHandler>
1188        });
1189        // trap_exit is set on the process BEFORE it is published as runnable
1190        // (0.16.1 `spawn_native_trap_exit` — pre-runnable by construction), so
1191        // an abnormal channel-actor crash is trapped (delivered as a message
1192        // the subscriber drains) instead of cascading across the link and
1193        // killing the subscriber. This makes the subscriber outlive a
1194        // channel-actor crash so the restarted actor can re-link to it on boot
1195        // (R2/R4): the flag is in place before the process's first slice, with
1196        // no post-spawn window for `set_trap_exit` to race (or return
1197        // `NoCaller` against a mid-first-slice process).
1198        let pid = scheduler.spawn_native_trap_exit(factory).map_err(|error| {
1199            LiminalError::SubscriptionFailed {
1200                message: format!("failed to spawn subscriber process: {error:?}"),
1201            }
1202        })?;
1203        let handle = Self {
1204            inner: Arc::new(SubscriptionInner {
1205                pid,
1206                inbox: Arc::clone(&inbox),
1207                scheduler: Arc::clone(scheduler),
1208                refill: OnceLock::new(),
1209                refilling: Mutex::new(()),
1210            }),
1211        };
1212        let registration = SubscriberRegistration {
1213            pid,
1214            inbox,
1215            predicate,
1216        };
1217        Ok((handle, registration))
1218    }
1219
1220    /// Returns the beamr pid of the subscriber process this handle owns.
1221    #[must_use]
1222    pub(crate) fn pid(&self) -> u64 {
1223        self.inner.pid
1224    }
1225
1226    /// Attempts to receive the next delivered envelope without blocking.
1227    ///
1228    /// # Errors
1229    ///
1230    /// Returns [`LiminalError::SubscriptionFailed`] when the subscription inbox cannot be read.
1231    pub fn try_next(&self) -> Result<Option<Envelope>, LiminalError> {
1232        // Dequeue releases the envelope's admitted bytes back to the shared
1233        // connection budget (§5 charge/release symmetry). It is ALSO the A1 v1
1234        // credit event (§2): the consumer has taken the message out of the
1235        // bus's custody, so the derived bands the next admission reads are one
1236        // envelope freer. There is no separate credit ledger to keep in step.
1237        let next = self.inner.inbox.pop();
1238        // A1 §4 auto-catch-up: the credit this pop released may have taken a
1239        // lagging subscriber below its low watermark. Refilling here — on the
1240        // draining caller's thread, never on the channel actor's command slice
1241        // — is what lets a lagging durable subscriber converge with no
1242        // re-subscribe. A no-op (one atomic-ish lock read) for every subscriber
1243        // that is not lagging, which is every ephemeral subscriber always.
1244        self.refill_if_lagging();
1245        Ok(next)
1246    }
1247
1248    /// Seeds this subscription's replay cursor at the durable log head it is
1249    /// joining at (A1 §4). Called by the durable subscribe path BEFORE the
1250    /// registration reaches the channel actor, so the first publish this
1251    /// subscription can possibly see is already measured against the right
1252    /// origin.
1253    pub(crate) fn seed_replay_cursor(&self, head: u64) {
1254        self.inner.inbox.seed_replay_cursor(head);
1255    }
1256
1257    /// Attaches the durable catch-up source (A1 §4). Called once by the durable
1258    /// subscribe path before the handle is returned; a second call is a
1259    /// mis-wiring of that path and trips a debug assertion rather than being
1260    /// silently ignored.
1261    pub(crate) fn attach_durable_refill(
1262        &self,
1263        store: Arc<dyn DurableStore>,
1264        stream_key: String,
1265        schema_id: SchemaId,
1266        predicate: Option<SubscriptionPredicate>,
1267    ) {
1268        let attached = self.inner.refill.set(DurableRefill {
1269            store,
1270            stream_key,
1271            schema_id,
1272            predicate,
1273        });
1274        debug_assert!(
1275            attached.is_ok(),
1276            "the refill OnceLock is set exactly once per attach: attach_durable_refill \
1277             was called twice on one subscription, which only the durable subscribe \
1278             path may call and only before the handle is returned"
1279        );
1280    }
1281
1282    /// Replays the missed range into the bounded inbox until this subscriber is
1283    /// caught up or its free band is full (A1 §4, graft §0.4).
1284    ///
1285    /// **Loop-until-caught-up.** The log head can move under concurrent
1286    /// appends, so "caught up" is not a snapshot taken once: each iteration
1287    /// re-reads from the cursor, and the flag clears only when a read comes
1288    /// back empty AND no live push was shed while that read was in flight. The
1289    /// chase is bounded because the pre-append watermark throttles producers
1290    /// while any subscriber lags.
1291    ///
1292    /// **In-order, exactly once.** Live pushes stay shed while `lagging` is
1293    /// set, so nothing can overtake the replay; the cursor advances only past
1294    /// entries this call actually offered, so nothing is offered twice.
1295    ///
1296    /// Errors are swallowed on purpose: a refill is opportunistic recovery
1297    /// running inside a consumer's `try_next`, and a store read failure must
1298    /// not turn a successful dequeue into an error. The subscriber stays
1299    /// `lagging` and the next pop retries, which is the same convergence with a
1300    /// longer gap.
1301    fn refill_if_lagging(&self) {
1302        let Some(refill) = self.inner.refill.get() else {
1303            return;
1304        };
1305        if !self.inner.inbox.is_lagging() {
1306            return;
1307        }
1308        // Another thread draining this same subscription is already refilling
1309        // it; a second concurrent replay would read the same range twice.
1310        let Ok(_guard) = self.inner.refilling.try_lock() else {
1311            return;
1312        };
1313        while let Some((generation, cursor, budget)) = self.inner.inbox.refill_plan() {
1314            let Ok(Ok(batch)) = block_on(replay_range(
1315                refill.store.as_ref(),
1316                &refill.stream_key,
1317                cursor,
1318                budget,
1319            )) else {
1320                return;
1321            };
1322            if batch.is_empty() {
1323                // The read reached the head. Clear the gap ONLY if no live push
1324                // was shed since the generation was taken; otherwise loop and
1325                // read again for the message that shed.
1326                if self.inner.inbox.clear_lagging_if_unchanged(generation) {
1327                    return;
1328                }
1329                continue;
1330            }
1331            for (sequence, stored) in batch {
1332                let envelope = refilled_envelope(&stored, refill.schema_id);
1333                let matched = refill
1334                    .predicate
1335                    .as_ref()
1336                    .is_none_or(|predicate| predicate(&envelope));
1337                if matched {
1338                    // Through the SAME bounded admission as a live push. The
1339                    // lagging flag is still set, so `admit_at` would shed this
1340                    // as a live push — the refill therefore goes in through the
1341                    // replay door below, which is the only writer allowed to
1342                    // move the cursor while the gap is open.
1343                    if !self.inner.inbox.admit_replayed(envelope, sequence) {
1344                        return;
1345                    }
1346                } else {
1347                    self.inner.inbox.note_replayed_filtered(sequence);
1348                }
1349            }
1350        }
1351    }
1352
1353    /// Whether an envelope is available without consuming it.
1354    #[must_use]
1355    pub fn has_pending(&self) -> bool {
1356        self.inner.inbox.has_pending()
1357    }
1358
1359    /// Whether an overflow has marked this subscription for shedding (§5).
1360    #[must_use]
1361    pub fn is_overflowed(&self) -> bool {
1362        self.inner.inbox.is_overflowed()
1363    }
1364}
1365
1366impl std::fmt::Debug for SubscriptionHandle {
1367    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1368        formatter
1369            .debug_struct("SubscriptionHandle")
1370            .field("pid", &self.inner.pid)
1371            .finish_non_exhaustive()
1372    }
1373}
1374
1375impl Drop for SubscriptionInner {
1376    fn drop(&mut self) {
1377        // Close the inbox FIRST: atomically mark it closed, drain queued entries,
1378        // and release every charged byte back to the shared connection budget
1379        // (§5 — queued bytes must never be stranded on the connection-lifetime
1380        // budget by unsubscribe, shed, or teardown; all of them funnel through
1381        // this drop). Post-close deliveries from the channel actor (whose EXIT
1382        // prune below is asynchronous) are refused without charging.
1383        self.inbox.close();
1384        // Terminating the subscriber process fires the bidirectional link to the
1385        // channel actor, which traps the EXIT and removes this subscriber from
1386        // its fan-out list. This is the real-beamr unsubscribe-on-drop path.
1387        self.scheduler
1388            .terminate_process(self.pid, ExitReason::Normal);
1389    }
1390}
1391
1392/// Rebuilds a bus [`Envelope`] from a durable log entry, for the host-side
1393/// refill (A1 §4).
1394///
1395/// Two fields are deliberately NOT reconstructed, because the log does not hold
1396/// them and inventing them would make a refilled envelope claim more than the
1397/// store can support:
1398///
1399/// * **causal context** — the durable publish path persists `None` for it
1400///   (`ChannelHandle::persist_and_enqueue`), so there is nothing to restore;
1401/// * **message id** — the durable record does not store the live envelope's id,
1402///   so a fresh one is minted. A consumer that needs to correlate a replayed
1403///   message with a live one uses its durable position, which is exact.
1404fn refilled_envelope(stored: &MessageEnvelope, schema_id: SchemaId) -> Envelope {
1405    let millis = i64::try_from(stored.timestamp).unwrap_or(i64::MAX);
1406    let timestamp = chrono::TimeZone::timestamp_millis_opt(&chrono::Utc, millis)
1407        .single()
1408        .unwrap_or_else(chrono::Utc::now);
1409    Envelope::with_timestamp(
1410        stored.payload.clone(),
1411        None,
1412        schema_id,
1413        PublisherId::new(stored.publisher_id.clone()),
1414        timestamp,
1415    )
1416}
1417
1418/// WR-9b: the REAL [`SubscriberProcess`] running on beamr's cooperative
1419/// (single-threaded / wasm) [`beamr::scheduler::WasmScheduler`].
1420///
1421/// This proves the production subscriber handler — the same `NativeHandler` the
1422/// threaded [`SubscriptionHandle::spawn`] spawns — runs unchanged on the
1423/// cooperative scheduler that a browser host drives. There is no toy stand-in:
1424/// the test spawns the genuine [`SubscriberProcess`], delivers a genuine
1425/// [`crate::channel::wire::encode_envelope`] frame as a real beamr binary, pumps
1426/// cooperative `run_until_idle` turns, and asserts the envelope is decoded by the
1427/// handler's own `accept_remote_frame` path and lands in the shared inbox a
1428/// [`SubscriptionHandle::try_next`] would read.
1429///
1430/// The handler runs cooperatively AS-IS: its `handle` only touches
1431/// platform-neutral [`NativeContext`] capabilities (`set_trap_exit`, `recv`),
1432/// [`BinaryRef`], and [`decode_envelope`] — none of which reach for threads,
1433/// tokio, sockets, or a `SharedState`. The only wiring the smoke supplies is the
1434/// cooperative driver (spawn + owned-binary delivery + turn pump), exactly the
1435/// host-side seam the threaded `SubscriptionHandle`/channel-actor provide on
1436/// native.
1437#[cfg(test)]
1438#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1439mod cooperative_smoke {
1440    use std::cell::RefCell;
1441    use std::rc::Rc;
1442    use std::sync::Arc;
1443
1444    use beamr::atom::AtomTable;
1445    use beamr::ets::copy_term_to_ets;
1446    use beamr::module::ModuleRegistry;
1447    use beamr::native::BifRegistryImpl;
1448    use beamr::process::heap::Heap;
1449    use beamr::scheduler::WasmScheduler;
1450    use beamr::term::shared_binary::{SharedBinary, write_proc_bin};
1451
1452    use super::{SubscriberInbox, SubscriberProcess, SubscriptionInbox};
1453    use crate::channel::SchemaId;
1454    use crate::channel::wire::encode_envelope;
1455    use crate::envelope::{Envelope, PublisherId};
1456
1457    /// Build a cooperative scheduler the way a wasm host holds it (single
1458    /// `Rc<RefCell<…>>` on one thread).
1459    fn cooperative_scheduler() -> Rc<RefCell<WasmScheduler>> {
1460        let atom_table = Arc::new(AtomTable::with_common_atoms());
1461        let modules = Arc::new(ModuleRegistry::new());
1462        let bifs = Arc::new(BifRegistryImpl::new());
1463        Rc::new(RefCell::new(WasmScheduler::new(atom_table, modules, bifs)))
1464    }
1465
1466    /// Encode `envelope` into the production wire frame and wrap it as a
1467    /// heap-independent beamr binary term ready for `send_owned`, mirroring how a
1468    /// remote node hands a published frame to a subscriber pid (SRV-005).
1469    fn frame_as_owned_binary(envelope: &Envelope) -> beamr::ets::OwnedTerm {
1470        let bytes = encode_envelope(envelope);
1471        let shared = SharedBinary::new(bytes);
1472        // A ProcBin reference needs three heap words; copy it into ETS-owned
1473        // memory so the scratch heap can be dropped before delivery.
1474        let mut scratch = Heap::new(8);
1475        let words = scratch
1476            .alloc_slice(3)
1477            .expect("scratch heap holds a proc-bin reference");
1478        let term = write_proc_bin(words, &shared).expect("proc-bin term writes");
1479        copy_term_to_ets(term).expect("frame copies into an owned binary")
1480    }
1481
1482    fn sample_envelope() -> Envelope {
1483        // A whole-millisecond timestamp so the round-trip through the wire codec
1484        // (which carries millisecond resolution, see `channel::wire`) is exact;
1485        // `Utc::now()` sub-millisecond precision would otherwise be truncated on
1486        // decode and is irrelevant to what this smoke proves.
1487        let timestamp = chrono::TimeZone::timestamp_millis_opt(&chrono::Utc, 1_700_000_000_123)
1488            .single()
1489            .expect("valid fixed millisecond timestamp");
1490        Envelope::with_timestamp(
1491            b"{\"value\":42}".to_vec(),
1492            None,
1493            SchemaId::new(),
1494            PublisherId::from("publisher-cooperative"),
1495            timestamp,
1496        )
1497    }
1498
1499    #[test]
1500    fn real_subscriber_process_delivers_a_published_envelope_cooperatively() {
1501        let scheduler = cooperative_scheduler();
1502
1503        // The shared inbox the subscriber pushes decoded envelopes onto — the
1504        // exact channel the threaded `SubscriptionHandle::try_next` reads.
1505        let inbox: SubscriberInbox = SubscriptionInbox::new();
1506        let process_inbox = Arc::clone(&inbox);
1507
1508        // Spawn the GENUINE production subscriber handler as a first-class native
1509        // process on the cooperative scheduler.
1510        let pid = scheduler.borrow_mut().spawn_native_root(Box::new(move || {
1511            Box::new(SubscriberProcess {
1512                inbox: Arc::clone(&process_inbox),
1513            }) as Box<dyn beamr::native::native_process::NativeHandler>
1514        }));
1515
1516        // First turn: the handler runs once, asserts trap_exit, finds an empty
1517        // mailbox, and parks (`Wait`). No envelope has been delivered yet.
1518        scheduler.borrow_mut().run_until_idle();
1519        assert_eq!(
1520            inbox.len(),
1521            0,
1522            "no envelope is delivered before one is published"
1523        );
1524
1525        // Publish: deliver a real encoded frame as a beamr binary straight to the
1526        // subscriber pid, exactly as a remote publish lands (SRV-005). This wakes
1527        // the parked process.
1528        let published = sample_envelope();
1529        let frame = frame_as_owned_binary(&published);
1530        scheduler
1531            .borrow_mut()
1532            .send_owned(pid, &frame)
1533            .expect("frame is delivered to the live subscriber pid");
1534
1535        // Pump turns: the woken handler drains the binary, decodes it through its
1536        // own `accept_remote_frame` path, and pushes the envelope onto the inbox.
1537        let mut delivered = None;
1538        for _ in 0..8 {
1539            scheduler.borrow_mut().run_until_idle();
1540            let next = inbox.pop();
1541            if let Some(envelope) = next {
1542                delivered = Some(envelope);
1543                break;
1544            }
1545        }
1546
1547        assert_eq!(
1548            delivered.as_ref(),
1549            Some(&published),
1550            "the real subscriber decoded and delivered the published envelope"
1551        );
1552    }
1553}
1554
1555/// R3 (§1.2(2)) + §5 inbox-bounding library core: the notifier fires on every
1556/// admitted envelope; the shared byte budget is spent across ALL a
1557/// connection's inboxes; overflow sheds the offending subscription; the per-inbox
1558/// fairness trip stops one inbox starving its siblings; and charge/release is
1559/// exact. These exercise [`SubscriptionInbox`]/[`ConnectionInboxBudget`] directly,
1560/// with no scheduler — the server-side wake wiring and shed are tested there.
1561#[cfg(test)]
1562#[allow(clippy::expect_used)]
1563mod inbox_bounding {
1564    use std::sync::Arc;
1565    use std::sync::atomic::{AtomicUsize, Ordering};
1566
1567    use super::{ConnectionInboxBudget, InboxAdmission, SubscriptionInbox};
1568    use crate::channel::SchemaId;
1569    use crate::channel::wire::encode_envelope;
1570    use crate::envelope::{Envelope, PublisherId};
1571    use crate::pressure::ConsumerCapacity;
1572
1573    fn envelope(payload: &[u8]) -> Envelope {
1574        Envelope::new(
1575            payload.to_vec(),
1576            None,
1577            SchemaId::new(),
1578            PublisherId::from("inbox-bounding-test"),
1579        )
1580    }
1581
1582    fn admitted_bytes(env: &Envelope) -> usize {
1583        encode_envelope(env).len()
1584    }
1585
1586    /// The level-triggered wake contract (P0 #55). This test previously asserted
1587    /// the edge-triggered form — that a second admit into a non-empty inbox does
1588    /// NOT re-fire — which is precisely the starvation the fix removes: the
1589    /// consumer drains a bounded slice, so a non-empty inbox is the normal state
1590    /// of a subscriber that has fallen behind, and withholding its wake is what
1591    /// ratchets it to the depth cap and a permanent shed.
1592    #[test]
1593    fn notifier_fires_for_every_admitted_envelope() {
1594        let inbox = SubscriptionInbox::new();
1595        let fires = Arc::new(AtomicUsize::new(0));
1596        let counter = Arc::clone(&fires);
1597        inbox.install_notifier(Arc::new(move || {
1598            counter.fetch_add(1, Ordering::Relaxed);
1599        }));
1600
1601        // First admit into an empty inbox fires.
1602        assert!(inbox.admit(envelope(b"a")).is_queued());
1603        assert_eq!(fires.load(Ordering::Relaxed), 1, "the first admit fires");
1604
1605        // A second admit into a STILL-NON-EMPTY inbox fires again: the consumer
1606        // may not have reached this envelope's slice, and R6 coalescing means a
1607        // redundant marker costs one mailbox atom, never a second slice of work.
1608        assert!(inbox.admit(envelope(b"b")).is_queued());
1609        assert_eq!(
1610            fires.load(Ordering::Relaxed),
1611            2,
1612            "an admit into a non-empty inbox still fires"
1613        );
1614
1615        // Drain to empty, then admit again: still exactly one fire per admit.
1616        assert!(inbox.pop().is_some());
1617        assert!(inbox.pop().is_some());
1618        assert!(inbox.admit(envelope(b"c")).is_queued());
1619        assert_eq!(
1620            fires.load(Ordering::Relaxed),
1621            3,
1622            "one fire per admitted envelope, whatever the queue depth was"
1623        );
1624    }
1625
1626    #[test]
1627    fn shared_budget_is_spent_across_all_a_connections_inboxes() {
1628        let one = envelope(b"payload-one");
1629        let two = envelope(b"payload-two");
1630        // A budget large enough for exactly ONE of the two envelopes.
1631        let cap = admitted_bytes(&one);
1632        let budget = ConnectionInboxBudget::new(cap);
1633
1634        let inbox_a = SubscriptionInbox::new();
1635        let inbox_b = SubscriptionInbox::new();
1636        inbox_a.install_budget(Arc::clone(&budget), usize::MAX);
1637        inbox_b.install_budget(Arc::clone(&budget), usize::MAX);
1638
1639        // Inbox A admits its envelope, consuming the whole shared budget.
1640        assert!(inbox_a.admit(one).is_queued());
1641        assert_eq!(budget.used(), cap, "the shared budget is now fully spent");
1642
1643        // Inbox B — a SIBLING subscription — is refused: the budget is connection
1644        // scoped, not per-inbox, so A's fill denies B.
1645        assert!(matches!(
1646            inbox_b.admit(two),
1647            InboxAdmission::BudgetExceeded(_)
1648        ));
1649        assert!(
1650            inbox_b.is_overflowed(),
1651            "the sibling that overflowed the shared budget is shed"
1652        );
1653        assert!(!inbox_a.is_overflowed(), "the inbox that fit is not shed");
1654
1655        // Draining A releases its bytes back to the SHARED budget, so B could then
1656        // admit (charge/release symmetry across siblings).
1657        assert!(inbox_a.pop().is_some());
1658        assert_eq!(
1659            budget.used(),
1660            0,
1661            "release returns bytes to the shared budget"
1662        );
1663    }
1664
1665    #[test]
1666    fn overflow_sheds_and_does_not_grow_memory() {
1667        let env = envelope(b"x");
1668        let budget = ConnectionInboxBudget::new(admitted_bytes(&env)); // room for one
1669        let inbox = SubscriptionInbox::new();
1670        inbox.install_budget(budget, usize::MAX);
1671
1672        assert!(inbox.admit(env.clone()).is_queued());
1673        // The next admit overflows: refused, marked for shedding, and NOT queued —
1674        // the queue length does not grow past the bound.
1675        assert!(matches!(
1676            inbox.admit(env),
1677            InboxAdmission::BudgetExceeded(_)
1678        ));
1679        assert!(inbox.is_overflowed());
1680        assert_eq!(
1681            inbox.len(),
1682            1,
1683            "the overflowed envelope is dropped, not queued"
1684        );
1685    }
1686
1687    #[test]
1688    fn per_inbox_fairness_trip_stops_one_inbox_starving_siblings() {
1689        // A huge byte budget so the FAIRNESS count — not the budget — is the trip.
1690        let budget = ConnectionInboxBudget::new(usize::MAX);
1691        let inbox = SubscriptionInbox::new();
1692        inbox.install_budget(budget, 2); // depth cap of 2 envelopes
1693
1694        assert!(inbox.admit(envelope(b"1")).is_queued());
1695        assert!(inbox.admit(envelope(b"2")).is_queued());
1696        // The third trips the fairness cap even though bytes are available.
1697        assert!(matches!(
1698            inbox.admit(envelope(b"3")),
1699            InboxAdmission::FairnessTripped(_)
1700        ));
1701        assert!(inbox.is_overflowed());
1702        assert_eq!(
1703            inbox.len(),
1704            2,
1705            "the fairness trip holds the inbox at its cap"
1706        );
1707    }
1708
1709    #[test]
1710    fn charge_and_release_are_exact() {
1711        let budget = ConnectionInboxBudget::new(1024 * 1024);
1712        let inbox = SubscriptionInbox::new();
1713        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1714
1715        let a = envelope(b"first-envelope");
1716        let b = envelope(b"second-longer-envelope-payload");
1717        let charge = admitted_bytes(&a) + admitted_bytes(&b);
1718        assert!(inbox.admit(a).is_queued());
1719        assert!(inbox.admit(b).is_queued());
1720        assert_eq!(budget.used(), charge, "used == sum of admitted bytes");
1721
1722        assert!(inbox.pop().is_some());
1723        assert!(inbox.pop().is_some());
1724        assert_eq!(
1725            budget.used(),
1726            0,
1727            "every admitted byte is released on dequeue — exact symmetry"
1728        );
1729    }
1730
1731    /// Review round 1 item 4: closing an inbox with a QUEUED backlog (the shed
1732    /// shape — an overflowed inbox is near-full by construction) releases every
1733    /// charged byte back to the shared budget, so a sibling subscription can
1734    /// admit again. Without the close-release, one shed strands its whole share
1735    /// of the 4 MiB budget forever.
1736    #[test]
1737    fn close_releases_queued_charges_so_siblings_recover() {
1738        // Budget sized so inbox A can queue a 256-envelope backlog (the §5
1739        // fairness-cap depth) and exhaust the shared budget doing it.
1740        let one = envelope(b"backlog-envelope-payload");
1741        let unit = admitted_bytes(&one);
1742        let budget = ConnectionInboxBudget::new(unit * 256);
1743
1744        let inbox_a = SubscriptionInbox::new();
1745        let inbox_b = SubscriptionInbox::new();
1746        inbox_a.install_budget(Arc::clone(&budget), usize::MAX);
1747        inbox_b.install_budget(Arc::clone(&budget), usize::MAX);
1748
1749        // Queue the full 256-envelope backlog on A, consuming the whole budget.
1750        for _ in 0..256 {
1751            assert!(inbox_a.admit(one.clone()).is_queued());
1752        }
1753        assert_eq!(budget.used(), unit * 256, "the backlog holds the budget");
1754        // The sibling is starved (the shed trigger condition).
1755        assert!(matches!(
1756            inbox_b.admit(one.clone()),
1757            InboxAdmission::BudgetExceeded(_)
1758        ));
1759
1760        // Shed/unsubscribe/teardown all funnel through close: EVERY queued charge
1761        // returns to the shared budget in one atomic close.
1762        inbox_a.close();
1763        assert_eq!(
1764            budget.used(),
1765            0,
1766            "close releases the entire queued backlog back to the shared budget"
1767        );
1768        // The sibling recovers: it can admit again.
1769        assert!(
1770            inbox_b.admit(one).is_queued(),
1771            "a sibling admits again after the other inbox is shed"
1772        );
1773    }
1774
1775    /// Review round 1 item 4: a closed inbox refuses admissions WITHOUT charging
1776    /// the budget, so a shed subscription can never re-accumulate cost while the
1777    /// channel actor's asynchronous EXIT prune is still in flight.
1778    #[test]
1779    fn closed_inbox_refuses_without_charging() {
1780        let env = envelope(b"post-close");
1781        let budget = ConnectionInboxBudget::new(1024 * 1024);
1782        let inbox = SubscriptionInbox::new();
1783        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1784
1785        inbox.close();
1786        assert_eq!(inbox.admit(env), InboxAdmission::Closed);
1787        assert_eq!(budget.used(), 0, "a closed inbox never charges the budget");
1788        assert_eq!(inbox.len(), 0, "a closed inbox never queues");
1789    }
1790
1791    /// Review round 1 item 4: the `Drop` backstop — if no teardown path ever
1792    /// called `close`, the last handle dropping still releases the queued charges
1793    /// (release-by-construction: a release that cannot be omitted).
1794    #[test]
1795    fn drop_backstop_releases_queued_charges() {
1796        let env = envelope(b"dropped-while-queued");
1797        let unit = admitted_bytes(&env);
1798        let budget = ConnectionInboxBudget::new(1024 * 1024);
1799        {
1800            let inbox = SubscriptionInbox::new();
1801            inbox.install_budget(Arc::clone(&budget), usize::MAX);
1802            assert!(inbox.admit(env.clone()).is_queued());
1803            assert!(inbox.admit(env).is_queued());
1804            assert_eq!(budget.used(), unit * 2);
1805            // No close() call: the Arc drops here.
1806        }
1807        assert_eq!(
1808            budget.used(),
1809            0,
1810            "dropping the last inbox handle releases every queued charge"
1811        );
1812    }
1813
1814    /// Review round 1 item 4: close is idempotent, and pop-after-close finds
1815    /// nothing (the queue was drained into the release).
1816    #[test]
1817    fn close_is_idempotent_and_drains_the_queue() {
1818        let env = envelope(b"x");
1819        let budget = ConnectionInboxBudget::new(1024 * 1024);
1820        let inbox = SubscriptionInbox::new();
1821        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1822        assert!(inbox.admit(env).is_queued());
1823
1824        inbox.close();
1825        inbox.close(); // second close is a no-op, not a double release
1826        assert_eq!(budget.used(), 0);
1827        assert!(inbox.pop().is_none(), "a closed inbox holds nothing");
1828    }
1829
1830    /// Review round 1 item 5 (charge ownership): an envelope admitted BEFORE the
1831    /// budget was installed carries a charge of 0 — its dequeue releases exactly
1832    /// 0 against the later-installed budget, never bytes it did not charge. The
1833    /// production subscribe path installs the budget at inbox construction so
1834    /// this window is structurally closed; this pins the defensive invariant
1835    /// that makes release byte-identical to charge on EVERY entry regardless.
1836    #[test]
1837    fn per_entry_charge_ownership_survives_budget_install() {
1838        let uncharged = envelope(b"admitted-before-budget-install");
1839        let charged = envelope(b"admitted-after-budget-install");
1840        let inbox = SubscriptionInbox::new();
1841
1842        // Admitted with no budget installed: charge ownership 0.
1843        assert!(inbox.admit(uncharged).is_queued());
1844
1845        let budget = ConnectionInboxBudget::new(1024 * 1024);
1846        inbox.install_budget(Arc::clone(&budget), usize::MAX);
1847        let unit = admitted_bytes(&charged);
1848        assert!(inbox.admit(charged).is_queued());
1849        assert_eq!(budget.used(), unit, "only the post-install entry charged");
1850
1851        // Popping the uncharged entry releases exactly 0 — the budget cannot
1852        // under-count (over-admitting past the signed 4 MiB) by releasing bytes
1853        // that were never charged.
1854        assert!(inbox.pop().is_some());
1855        assert_eq!(budget.used(), unit, "the uncharged entry released nothing");
1856        assert!(inbox.pop().is_some());
1857        assert_eq!(
1858            budget.used(),
1859            0,
1860            "the charged entry released its exact charge"
1861        );
1862    }
1863
1864    /// Review round 1 item 5 (install recheck): installing a notifier onto an
1865    /// ALREADY-NON-EMPTY inbox fires it exactly once — those envelopes were
1866    /// admitted while there was no notifier to fire, so the install regenerates
1867    /// their wake and one can never be lost to install ordering. (The production
1868    /// subscribe path installs at construction, when the queue is guaranteed
1869    /// empty; this pins the defensive invariant.)
1870    #[test]
1871    fn notifier_install_onto_non_empty_inbox_fires_once() {
1872        let inbox = SubscriptionInbox::new();
1873        assert!(inbox.admit(envelope(b"pre-install")).is_queued());
1874
1875        let fires = Arc::new(AtomicUsize::new(0));
1876        let counter = Arc::clone(&fires);
1877        inbox.install_notifier(Arc::new(move || {
1878            counter.fetch_add(1, Ordering::Relaxed);
1879        }));
1880        assert_eq!(
1881            fires.load(Ordering::Relaxed),
1882            1,
1883            "install onto a non-empty inbox regenerates exactly one wake"
1884        );
1885
1886        // The install is a ONE-OFF regeneration, not an extra fire per admit: a
1887        // subsequent admit adds exactly its own one fire.
1888        assert!(inbox.admit(envelope(b"second")).is_queued());
1889        assert_eq!(fires.load(Ordering::Relaxed), 2);
1890    }
1891
1892    /// **PIN (round 2) — the exactly-once guard suppresses what the REPLAY DOOR
1893    /// offered, and nothing else.**
1894    ///
1895    /// A live push that arrives behind a higher-positioned sibling has been
1896    /// offered by nobody. Suppressing it is silent loss, and worse than the
1897    /// duplicate the guard exists to prevent: nothing is shed, so `lagging` is
1898    /// never set and auto-catch-up can never recover it, while
1899    /// [`InboxAdmission::is_queued`] still reports it to the producer as
1900    /// delivered.
1901    ///
1902    /// The publish path is ordered so that this cannot happen in production
1903    /// (the fan-out command is enqueued under the append lock). This pin is the
1904    /// inbox's OWN control on the same claim: whatever the publish path does,
1905    /// the guard's comparand may only be advanced by a door that genuinely made
1906    /// an offer, so the worst an inversion could ever cost is a duplicate —
1907    /// never a lost message.
1908    #[test]
1909    fn a_live_push_is_not_suppressed_by_a_higher_positioned_sibling() {
1910        let inbox = SubscriptionInbox::new();
1911
1912        assert!(
1913            inbox.admit_at(envelope(b"position-5"), Some(5)).is_queued(),
1914            "the higher-positioned push is queued"
1915        );
1916        let behind = inbox.admit_at(envelope(b"position-3"), Some(3));
1917        assert!(
1918            !matches!(behind, InboxAdmission::AlreadyOffered(_)),
1919            "position 3 was offered by NOBODY — its only sin is arriving behind \
1920             position 5, and suppressing it loses it silently; got {behind:?}"
1921        );
1922        assert!(behind.is_queued(), "position 3 must reach the queue");
1923        assert_eq!(
1924            inbox.queued_len(),
1925            2,
1926            "both live pushes are in the queue, in arrival order"
1927        );
1928    }
1929
1930    /// **PIN (round 2), the anti-vacuity partner** — the guard still fires for
1931    /// the position the replay door actually offered. Without this, the pin
1932    /// above passes on a guard that was simply deleted.
1933    #[test]
1934    fn a_live_push_the_replay_door_already_offered_is_suppressed() {
1935        let inbox = SubscriptionInbox::new();
1936
1937        assert!(
1938            inbox.admit_replayed(envelope(b"replayed-5"), 5),
1939            "the replay door offers position 5"
1940        );
1941        let live = inbox.admit_at(envelope(b"live-5"), Some(5));
1942        assert!(
1943            matches!(live, InboxAdmission::AlreadyOffered(_)),
1944            "position 5 was already offered by the refill; got {live:?}"
1945        );
1946        assert_eq!(
1947            inbox.queued_len(),
1948            1,
1949            "the suppressed push did not enter the queue a second time"
1950        );
1951    }
1952
1953    /// **PIN (round 2) — the refill cursor never backsteps onto a position that
1954    /// was already delivered.**
1955    ///
1956    /// The cursor names where the missed range STARTS. Assigning it
1957    /// `position + 1` unconditionally lets a live push that arrives behind a
1958    /// higher-positioned sibling drag it backwards over messages this
1959    /// subscriber already has, and the next refill then re-reads and re-offers
1960    /// them — the duplicate this whole seam exists to prevent, arriving through
1961    /// the other door.
1962    #[test]
1963    fn the_refill_cursor_never_backsteps_below_a_delivered_position() {
1964        let inbox = SubscriptionInbox::new();
1965        inbox
1966            .install_capacity(ConsumerCapacity::new(1, 1).expect("1/1 is a legal capacity"))
1967            .expect("a legal capacity installs");
1968
1969        assert!(inbox.admit_at(envelope(b"position-5"), Some(5)).is_queued());
1970        assert!(inbox.admit_at(envelope(b"position-3"), Some(3)).is_queued());
1971        // The bound (1 + 1) is now full, so this one is shed and OPENS the gap.
1972        assert!(matches!(
1973            inbox.admit_at(envelope(b"position-6"), Some(6)),
1974            InboxAdmission::Rejected(_)
1975        ));
1976        assert!(inbox.is_lagging(), "the shed opened a gap");
1977
1978        assert!(inbox.pop().is_some());
1979        assert!(inbox.pop().is_some());
1980        let (_generation, cursor, _budget) = inbox
1981            .refill_plan()
1982            .expect("a drained lagging inbox has a refill due");
1983        assert_eq!(
1984            cursor, 6,
1985            "the missed range starts above every delivered position; a cursor of \
1986             4 would send the refill back over positions 4 and 5, and position 5 \
1987             is already in this subscriber's hands"
1988        );
1989    }
1990}