Skip to main content

wyrd_runtime/
outbox.rs

1//! Generic non-blocking, per-tenant batched outbox.
2//!
3//! A server surface that must record derived work without delaying the request
4//! that produced it hands each item to [`Outbox::stage`], which only enqueues
5//! it. One background writer per outbox groups queued items by tenant and by
6//! the sink's [`OutboxSink::lane`], and writes each lane's whole backlog
7//! through its [`OutboxSink`] in one all-or-nothing call. Each lane has at
8//! most one write in flight, so items of one lane are written in arrival
9//! order; a tenant's lanes write concurrently. At most `concurrency` writes
10//! run at once, and the tenant with the fewest writes in flight is
11//! dispatched first, so a busy tenant cannot hold every slot while another
12//! waits, and a slow tenant delays only its own items.
13//!
14//! A sink may set [`OutboxSink::LINGER`], as a Kafka producer sets
15//! `linger.ms`: a lane's backlog then waits until its oldest item is that old,
16//! or until it holds [`OutboxSink::BATCH_ITEMS`] items or
17//! [`OutboxSink::BATCH_BYTES`] accounted bytes, before it is written, so items
18//! staged one at a time reach the sink as one write instead of one write each.
19//! One write takes at most [`OutboxSink::BATCH_BYTES`] of a lane's backlog, so
20//! a large backlog is written, and its capacity released, a bounded slice at a
21//! time. Linger delays only when an item is written, never its
22//! producer, and a retry or a closed outbox writes at once.
23//!
24//! A failed write is never dropped. A write that returns an error or panics
25//! keeps its exact items as that lane's next write, apart from anything that
26//! arrived meanwhile, and the lane is retried after a backoff that starts at
27//! [`INITIAL_BACKOFF`] and doubles up to [`MAX_BACKOFF`]; other lanes keep
28//! writing. Later arrivals wait for the write after the retry succeeds, and
29//! every attempt of a slice carries the key the slice was given when it was
30//! first written, so a sink deriving identity from that key presents the same
31//! identity on every attempt. Because a write may be retried after an unknown
32//! commit outcome, every sink write must be safe to repeat.
33//!
34//! Every outbox has finite [`OutboxLimits`]: a most admitted items and a
35//! most admitted bytes, as each item's [`OutboxSink::weight`]. Admitted
36//! items are those queued, waiting, retrying, or being written, plus slots
37//! reserved and not yet staged. An item stays admitted until it is written
38//! or lost, never merely dequeued, and a retried slice is admitted once
39//! however often it is retried. Both limits are split in two shares. A
40//! producer that must refuse its caller rather than lose an item takes a
41//! [`Reservation`] first; [`Outbox::reserve`] refuses with [`OutboxFull`]
42//! once reserved slots and the unwritten items staged through them would
43//! exceed [`RESERVED_SHARE`] of the item limit, or the bytes of those items
44//! already reach that share of the byte limit. Unreserved items from
45//! [`Outbox::stage`] hold the rest of both, so neither share can crowd out
46//! the other. [`Outbox::reserve_until`] instead waits for capacity in arrival
47//! order, woken whenever admitted items are released. `stage` never fails or
48//! waits: past either unreserved limit an
49//! item is shed, counted lost, and logged at most once per tenant every
50//! [`SHED_LOG_INTERVAL`] with the count shed since the last record, so a
51//! full outbox never blocks or fails the operation staging it.
52//!
53//! Nothing is preallocated. Every lost item is counted once in
54//! `outbox_events_lost_total{outbox, reason}` with its [`LossReason`]: shed at
55//! an unreserved limit, staged after shutdown began or unwritten at its
56//! deadline, or consumed unwritten by the sink. Items lost when the process
57//! stops abruptly are never counted. Written items and their bytes are
58//! counted in `outbox_items_written_total{outbox}` and
59//! `outbox_bytes_written_total{outbox}`, every failed write attempt in
60//! `outbox_write_failures_total{outbox}`, and the items not yet written are
61//! exported as the `outbox_pending{outbox}` gauge.
62
63use std::any::Any;
64use std::collections::{HashMap, VecDeque};
65use std::fmt::Display;
66use std::future::{Future, pending, poll_fn};
67use std::panic::{AssertUnwindSafe, catch_unwind};
68use std::pin::pin;
69use std::sync::atomic::{AtomicUsize, Ordering, fence};
70use std::sync::{Arc, Mutex, PoisonError, RwLock};
71use std::task::Poll;
72use std::time::{Duration, Instant};
73
74use tokio::sync::{Notify, mpsc};
75use tokio::task::{Id, JoinError, JoinSet};
76use tokio::time::{sleep_until, timeout_at};
77use tokio_util::sync::CancellationToken;
78use tokio_util::task::TaskTracker;
79use uuid::Uuid;
80use wyrd_spec::DataTenantId;
81
82/// Delay before the first retry of a lane whose write failed.
83pub const INITIAL_BACKOFF: Duration = Duration::from_millis(50);
84
85/// Longest delay between retries of a tenant whose writes keep failing.
86pub const MAX_BACKOFF: Duration = Duration::from_secs(5);
87
88/// Share of an outbox's capacity, as a fraction `(numerator, denominator)`,
89/// that reservations may hold; the rest is unreserved staging's, so a
90/// refusing producer's burst cannot crowd out audit decisions and their
91/// traffic cannot refuse the producer.
92pub const RESERVED_SHARE: (usize, usize) = (3, 4);
93
94/// Shortest time between two shed records of one outbox and tenant.
95///
96/// A saturated producer sheds every item it stages; one record per interval
97/// carrying the count keeps the log readable and its cost flat.
98pub const SHED_LOG_INTERVAL: Duration = Duration::from_secs(1);
99
100/// The most an [`Outbox`] admits at once, by item count and by accounted
101/// bytes; reaching either limit of a share sheds or refuses.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct OutboxLimits {
104    /// Most items admitted, including unspent reserved slots.
105    pub items: usize,
106    /// Most bytes admitted, as the sum of [`OutboxSink::weight`] over the
107    /// admitted items.
108    pub bytes: usize,
109}
110
111/// Why an outbox item was lost, the `reason` label of
112/// `outbox_events_lost_total`.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum LossReason {
115    /// Refused at an unreserved staging limit.
116    Shed,
117    /// Consumed by the sink without being written: a terminal refusal or an
118    /// item that could not be encoded, which no retry could cure.
119    Rejected,
120    /// The process reaches no destination to write to.
121    Unreachable,
122    /// Staged after shutdown began, or still unwritten at its deadline.
123    Shutdown,
124}
125
126impl LossReason {
127    /// The fixed metric label value.
128    #[must_use]
129    pub const fn as_str(self) -> &'static str {
130        match self {
131            Self::Shed => "shed",
132            Self::Rejected => "rejected",
133            Self::Unreachable => "unreachable",
134            Self::Shutdown => "shutdown",
135        }
136    }
137}
138
139/// Refusal of a [`Reservation`] because the outbox holds its reserved share.
140///
141/// Nothing was admitted, so the caller may retry the whole operation later.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
143#[error("outbox {outbox} is full: {admitted} of {limit} reservable {unit} admitted")]
144pub struct OutboxFull {
145    /// The refusing outbox's [`OutboxSink::NAME`].
146    pub outbox: &'static str,
147    /// Reserved slots and unwritten reserved items, or the bytes of those
148    /// items, when the reservation was refused.
149    pub admitted: usize,
150    /// Items or bytes reservations may hold in this outbox.
151    pub limit: usize,
152    /// Which limit refused: `"items"` or `"bytes"`.
153    pub unit: &'static str,
154}
155
156/// One destination an outbox writes to.
157///
158/// Implementations write everything one lane of a tenant has queued in one
159/// call: in one transaction, or as idempotent writes whose repeat is
160/// absorbed. The outbox owns queueing, grouping, concurrency, retry, and
161/// shutdown.
162pub trait OutboxSink: Send + Sync + 'static {
163    /// The item callers stage.
164    type Item: Send + 'static;
165    /// The failure a write reports; logged, counted, and retried.
166    type Error: Display + Send + 'static;
167    /// Label for this outbox's metrics and logs.
168    const NAME: &'static str;
169    /// Lanes each tenant's items are spread over. Lanes of one tenant write
170    /// concurrently; the default single lane writes a tenant one slice at a
171    /// time.
172    const LANES: usize = 1;
173    /// Longest a lane's oldest waiting item waits for more to join its write,
174    /// as Kafka's `linger.ms`. The default of zero writes as soon as a lane is
175    /// free.
176    const LINGER: Duration = Duration::ZERO;
177    /// Waiting items at which a lane writes without lingering further, as
178    /// Kafka's `batch.size`.
179    const BATCH_ITEMS: usize = usize::MAX;
180    /// Accounted bytes, as [`Self::weight`], at which a lane writes without
181    /// lingering further, and the most one write takes of its backlog, as
182    /// Kafka's `batch.size`. An item heavier than this is written alone. The
183    /// rest of the backlog stays queued in order and is written next, so a
184    /// slice's capacity returns to producers when that slice is written, not
185    /// when the whole backlog is.
186    const BATCH_BYTES: usize = usize::MAX;
187
188    /// The lane `item` is written in, taken modulo [`Self::LANES`].
189    ///
190    /// Items of one tenant and lane are written in arrival order, one write
191    /// at a time, so items whose writes must observe each other in arrival
192    /// order, such as requests sharing an idempotency key, must share a lane.
193    #[must_use]
194    fn lane(item: &Self::Item) -> u64 {
195        let _ = item;
196        0
197    }
198
199    /// The bytes `item` is accounted as against [`OutboxLimits::bytes`].
200    ///
201    /// At least the length of every payload the item holds, such as captured
202    /// request and response bodies; encoding overhead need not be counted.
203    /// The default of zero suits items bounded by their producer's input
204    /// limits, which the item limit then bounds.
205    #[must_use]
206    fn weight(item: &Self::Item) -> usize {
207        let _ = item;
208        0
209    }
210
211    /// Writes one lane's items of one tenant, in order, in one transaction.
212    ///
213    /// A write may be retried after its commit outcome is unknown, so writing
214    /// the same items twice must leave the same durable state as writing them
215    /// once. `slice` is the slice's idempotency key: a `UUIDv7` assigned when
216    /// the slice is first written and passed again with every retry of the
217    /// identical items, so a sink derives durable identities from it without
218    /// reading the items.
219    ///
220    /// # Errors
221    ///
222    /// Returns the sink's error when any part of the write fails; nothing of
223    /// the batch may then be durable unless a repeat would skip it.
224    fn write(
225        &self,
226        tenant: DataTenantId,
227        slice: Uuid,
228        items: &[Self::Item],
229    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
230}
231
232/// One queued item: its tenant, the item, and its admitted [`Load`].
233type Queued<S> = (DataTenantId, <S as OutboxSink>::Item, Load);
234
235/// Sending half of an outbox queue.
236type QueueSender<S> = mpsc::UnboundedSender<Queued<S>>;
237
238/// The handle callers stage through; owns the queue and its writer task.
239pub struct Outbox<S: OutboxSink> {
240    /// Sending half of the unbounded queue the writer drains, taken by
241    /// shutdown. `stage` sends under the read lock and shutdown takes it under
242    /// the write lock, so every item is either queued before the fence and
243    /// drained, or refused after it.
244    queue: RwLock<Option<QueueSender<S>>>,
245    /// Items queued, waiting, or being written, not yet written or lost.
246    pending: Arc<AtomicUsize>,
247    /// Admission counts the writer releases written and lost items from.
248    admission: Arc<Admission>,
249    /// Most items and bytes this outbox admits at once.
250    limits: OutboxLimits,
251    /// Per tenant, items shed since the last shed record and when that record
252    /// was written; see [`SHED_LOG_INTERVAL`].
253    // ponytail: one small entry per tenant that ever shed, never pruned;
254    // prune idle entries if tenant counts reach the millions.
255    shed: Mutex<HashMap<DataTenantId, ShedLog>>,
256    /// Signalled whenever a settled write or abandonment leaves nothing
257    /// pending.
258    idle: Arc<Notify>,
259    /// [`Self::reserve_until`] callers waiting for capacity, in arrival
260    /// order.
261    waiters: Mutex<Waiters>,
262    /// Asks the writer to give up at the shutdown deadline.
263    abandon: CancellationToken,
264    /// Tracks the writer so shutdown can wait for it.
265    writer: TaskTracker,
266    /// The sink the writer submits through, shared for inspection.
267    sink: Arc<S>,
268}
269
270/// Names the outbox and its pending count, so values carrying a handle — a
271/// staged write that hands off to a second outbox — stay debuggable.
272impl<S: OutboxSink> std::fmt::Debug for Outbox<S> {
273    /// Write the sink's name and a relaxed snapshot of the pending count;
274    /// queued items and the sink itself are never formatted.
275    ///
276    /// # Errors
277    /// Returns the formatter's error when it rejects a write.
278    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        formatter
280            .debug_struct("Outbox")
281            .field("name", &S::NAME)
282            .field("pending", &self.pending.load(Ordering::Relaxed))
283            .finish_non_exhaustive()
284    }
285}
286
287impl<S: OutboxSink> Outbox<S> {
288    /// Creates the outbox around `sink` and starts its writer.
289    ///
290    /// `concurrency` is the most writes run at once across every tenant and
291    /// lane, which bounds the connections the sink may hold; `limits` are the
292    /// most items and bytes admitted at once, which bound the memory queued,
293    /// waiting, retrying, and in-flight work may hold.
294    ///
295    /// # Panics
296    ///
297    /// Panics when called outside a Tokio runtime, because the writer task is
298    /// spawned immediately.
299    #[must_use]
300    pub fn new(sink: S, concurrency: usize, limits: OutboxLimits) -> Arc<Self> {
301        // The channel is unbounded because `admission` already bounds it.
302        let (queue, requests) = mpsc::unbounded_channel();
303        let pending = Arc::new(AtomicUsize::new(0));
304        let admission = Arc::new(Admission::default());
305        let idle = Arc::new(Notify::new());
306        let abandon = CancellationToken::new();
307        let writer = TaskTracker::new();
308        let sink = Arc::new(sink);
309        writer.spawn(
310            OutboxWriter {
311                sink: Arc::clone(&sink),
312                requests,
313                waiting: HashMap::new(),
314                since: HashMap::new(),
315                closed: false,
316                failed: HashMap::new(),
317                writing: JoinSet::new(),
318                in_flight: HashMap::new(),
319                retry_at: HashMap::new(),
320                concurrency: concurrency.max(1),
321                pending: Arc::clone(&pending),
322                admission: Arc::clone(&admission),
323                idle: Arc::clone(&idle),
324                abandon: abandon.clone(),
325            }
326            .run(),
327        );
328        writer.close();
329        Arc::new(Self {
330            queue: RwLock::new(Some(queue)),
331            pending,
332            admission,
333            limits,
334            shed: Mutex::default(),
335            idle,
336            waiters: Mutex::default(),
337            abandon,
338            writer,
339            sink,
340        })
341    }
342
343    /// Borrows the sink this outbox writes through.
344    #[must_use]
345    pub fn sink(&self) -> &S {
346        &self.sink
347    }
348
349    /// Queues one item for `tenant` without waiting and never fails.
350    ///
351    /// The item is admitted and counted pending before it is sent, so the
352    /// writer can never release it first. Admission happens under the queue's
353    /// read lock, so nothing is admitted once shutdown has taken the queue.
354    /// When either unreserved limit would be passed the item is shed: counted
355    /// lost with [`LossReason::Shed`] and recorded by [`Self::shed`]. Once
356    /// shutdown has begun, or if the writer is gone, the item is counted lost
357    /// with [`LossReason::Shutdown`] and logged.
358    pub fn stage(&self, tenant: DataTenantId, item: impl Into<S::Item>) {
359        let item = item.into();
360        let bytes = S::weight(&item);
361        let queue = self.queue.read().unwrap_or_else(PoisonError::into_inner);
362        let Some(sender) = queue.as_ref() else {
363            drop(queue);
364            lose_late::<S>(tenant);
365            return;
366        };
367        let admission = &self.admission;
368        if admit(
369            &admission.unreserved,
370            1,
371            self.limits.items - self.reservable(),
372        )
373        .is_err()
374        {
375            drop(queue);
376            self.shed(tenant);
377            return;
378        }
379        let byte_limit = self.limits.bytes - self.reservable_bytes();
380        if admit(&admission.unreserved_bytes, bytes, byte_limit).is_err() {
381            admission.unreserved.fetch_sub(1, Ordering::AcqRel);
382            drop(queue);
383            self.shed(tenant);
384            return;
385        }
386        self.enqueue(sender, tenant, item, Load::one(false, bytes));
387    }
388
389    /// Counts one shed item of `tenant` and writes a shed record when the
390    /// tenant's last one is at least [`SHED_LOG_INTERVAL`] old; the record
391    /// carries every item shed since the previous one.
392    fn shed(&self, tenant: DataTenantId) {
393        count_lost::<S>(1, LossReason::Shed);
394        let now = Instant::now();
395        let shed = {
396            let mut logs = self.shed.lock().unwrap_or_else(PoisonError::into_inner);
397            let log = logs.entry(tenant).or_default();
398            log.unlogged += 1;
399            if log
400                .logged_at
401                .is_some_and(|at| now.duration_since(at) < SHED_LOG_INTERVAL)
402            {
403                return;
404            }
405            log.logged_at = Some(now);
406            std::mem::take(&mut log.unlogged)
407        };
408        self.log_shed(tenant, shed);
409    }
410
411    /// Writes one shed record for `shed` items of `tenant`.
412    fn log_shed(&self, tenant: DataTenantId, shed: usize) {
413        tracing::error!(
414            outbox = S::NAME,
415            %tenant,
416            shed,
417            max_items = self.limits.items,
418            max_bytes = self.limits.bytes,
419            "outbox is full; staged items were shed"
420        );
421    }
422
423    /// Reserves `slots` items of capacity for a producer that must refuse its
424    /// caller instead of losing what it stages.
425    ///
426    /// The reservation is admitted at once and holds its slots until each is
427    /// staged through it or it is dropped, which releases the unused rest. An
428    /// item staged through it keeps its slot until it is written or lost.
429    /// Unreserved items never count against reservations.
430    ///
431    /// # Errors
432    ///
433    /// Returns [`OutboxFull`], admitting nothing, when the reservation would
434    /// take reservations past [`RESERVED_SHARE`] of the capacity.
435    pub fn reserve(self: &Arc<Self>, slots: usize) -> Result<Reservation<S>, OutboxFull> {
436        let byte_limit = self.reservable_bytes();
437        let bytes = self.admission.reserved_bytes.load(Ordering::Acquire);
438        if bytes >= byte_limit {
439            return Err(OutboxFull {
440                outbox: S::NAME,
441                admitted: bytes,
442                limit: byte_limit,
443                unit: "bytes",
444            });
445        }
446        let limit = self.reservable();
447        admit(&self.admission.reserved, slots, limit).map_err(|admitted| OutboxFull {
448            outbox: S::NAME,
449            admitted,
450            limit,
451            unit: "items",
452        })?;
453        Ok(Reservation {
454            outbox: Arc::clone(self),
455            slots: AtomicUsize::new(slots),
456        })
457    }
458
459    /// Reserves `slots` like [`Self::reserve`], waiting until `deadline` for
460    /// capacity instead of refusing at once.
461    ///
462    /// Waiters are admitted in arrival order. Under one short lock a caller
463    /// either reserves, when no one is waiting and the reservation fits, or
464    /// joins the back of the queue; only a queued caller suspends, so callers
465    /// that find capacity never wait on one another. Only the head of the
466    /// queue reserves, retrying whenever admitted items or unspent slots are
467    /// released, so it never sleeps past freed capacity and no later caller
468    /// takes capacity freed for it. A caller that leaves the queue, admitted,
469    /// timed out, or cancelled, wakes the next head.
470    ///
471    /// # Errors
472    ///
473    /// Returns the latest [`OutboxFull`] the queue's head was refused with,
474    /// admitting nothing, when `deadline` passes before the reservation fits.
475    pub async fn reserve_until(
476        self: &Arc<Self>,
477        slots: usize,
478        deadline: Instant,
479    ) -> Result<Reservation<S>, OutboxFull> {
480        let ticket = {
481            let mut waiters = self.waiters();
482            if waiters.queue.is_empty() {
483                match self.reserve(slots) {
484                    Ok(reservation) => return Ok(reservation),
485                    Err(full) if Instant::now() >= deadline => return Err(full),
486                    Err(full) => waiters.refused = Some(full),
487                }
488            }
489            waiters.join(&self.admission)
490        };
491        let _place = Place {
492            outbox: self,
493            ticket,
494        };
495        loop {
496            let freed = self.admission.freed.notified();
497            tokio::pin!(freed);
498            freed.as_mut().enable();
499            fence(Ordering::SeqCst);
500            let refused = {
501                let mut waiters = self.waiters();
502                if waiters.queue.front() == Some(&ticket) {
503                    match self.reserve(slots) {
504                        Ok(reservation) => return Ok(reservation),
505                        Err(full) => waiters.refused = Some(full),
506                    }
507                }
508                waiters.refused
509            };
510            if timeout_at(deadline.into(), freed).await.is_err() {
511                return Err(refused.unwrap_or(OutboxFull {
512                    outbox: S::NAME,
513                    admitted: self.admission.reserved.load(Ordering::Acquire),
514                    limit: self.reservable(),
515                    unit: "items",
516                }));
517            }
518        }
519    }
520
521    /// Locks the [`Self::reserve_until`] queue.
522    fn waiters(&self) -> std::sync::MutexGuard<'_, Waiters> {
523        self.waiters.lock().unwrap_or_else(PoisonError::into_inner)
524    }
525
526    /// The most slots reservations may hold: [`RESERVED_SHARE`] of the
527    /// item limit, rounded down.
528    const fn reservable(&self) -> usize {
529        self.limits.items * RESERVED_SHARE.0 / RESERVED_SHARE.1
530    }
531
532    /// The bytes at which reservations are refused: [`RESERVED_SHARE`] of
533    /// the byte limit, rounded down, computed without overflowing.
534    const fn reservable_bytes(&self) -> usize {
535        self.limits.bytes / RESERVED_SHARE.1 * RESERVED_SHARE.0
536    }
537
538    /// Sends one item on a spent reserved slot to the writer.
539    ///
540    /// Its bytes join the reserved share unconditionally, because the slot
541    /// already promised the producer its item is kept; [`Self::reserve`]
542    /// refuses new reservations once that share's bytes reach their limit.
543    /// Once shutdown has begun the item is released from its slot, counted
544    /// lost with [`LossReason::Shutdown`], and logged.
545    fn send_reserved(&self, tenant: DataTenantId, item: S::Item) {
546        let bytes = S::weight(&item);
547        let queue = self.queue.read().unwrap_or_else(PoisonError::into_inner);
548        let Some(sender) = queue.as_ref() else {
549            drop(queue);
550            self.admission.release(Load::one(true, 0));
551            lose_late::<S>(tenant);
552            return;
553        };
554        self.admission
555            .reserved_bytes
556            .fetch_add(bytes, Ordering::AcqRel);
557        self.enqueue(sender, tenant, item, Load::one(true, bytes));
558    }
559
560    /// Sends one admitted item to the writer, counting it pending first.
561    ///
562    /// Called under the queue's read lock. If the writer is gone, the item is
563    /// released from its shares, counted lost with [`LossReason::Shutdown`],
564    /// and logged.
565    fn enqueue(&self, sender: &QueueSender<S>, tenant: DataTenantId, item: S::Item, load: Load) {
566        let gauge = metrics::gauge!("outbox_pending", "outbox" => S::NAME);
567        self.pending.fetch_add(1, Ordering::AcqRel);
568        self.admission
569            .staged_reserved
570            .fetch_add(load.reserved, Ordering::AcqRel);
571        gauge.increment(1.0);
572        if sender.send((tenant, item, load)).is_ok() {
573            return;
574        }
575        self.pending.fetch_sub(1, Ordering::AcqRel);
576        self.admission
577            .staged_reserved
578            .fetch_sub(load.reserved, Ordering::AcqRel);
579        gauge.decrement(1.0);
580        self.admission.release(load);
581        lose_late::<S>(tenant);
582    }
583
584    /// Returns the number of items not yet written.
585    #[must_use]
586    pub fn pending(&self) -> usize {
587        self.pending.load(Ordering::Acquire)
588    }
589
590    /// Waits until `deadline` for everything staged so far to be written,
591    /// without closing the outbox, and returns how many items remain.
592    ///
593    /// The outbox keeps accepting items throughout; callers that read the
594    /// sink's destination right after staging use this to observe their items.
595    pub async fn settle(&self, deadline: Instant) -> usize {
596        loop {
597            let idle = self.idle.notified();
598            tokio::pin!(idle);
599            idle.as_mut().enable();
600            if self.pending() == 0 {
601                return 0;
602            }
603            if timeout_at(deadline.into(), idle).await.is_err() {
604                return self.pending();
605            }
606        }
607    }
608
609    /// Stops taking items, keeps writing and retrying until `deadline`, and
610    /// returns how many items were lost at the deadline.
611    ///
612    /// The staging sender is dropped before the first await, which is a
613    /// one-way fence: everything staged before it drains, and every later
614    /// [`Outbox::stage`] is refused and counted lost without entering the
615    /// queue. Shed counts not yet recorded are recorded at the fence.
616    /// Items still unwritten at the deadline are abandoned with the
617    /// writer, which cancels any in-flight write; once the writer is gone
618    /// they are counted in `outbox_events_lost_total` with
619    /// [`LossReason::Shutdown`], logged, and released
620    /// from `pending()` and `outbox_pending`, which both end at zero, and
621    /// released from their admission shares. When shutdown is called
622    /// concurrently, exactly one caller reports the loss and settles both
623    /// shares, and the others report zero. Unspent [`Reservation`] slots stay
624    /// held until their reservation drops.
625    pub async fn shutdown(&self, deadline: Instant) -> usize {
626        drop(
627            self.queue
628                .write()
629                .unwrap_or_else(PoisonError::into_inner)
630                .take(),
631        );
632        let unlogged: Vec<_> = self
633            .shed
634            .lock()
635            .unwrap_or_else(PoisonError::into_inner)
636            .iter_mut()
637            .filter(|(_, log)| log.unlogged > 0)
638            .map(|(tenant, log)| (*tenant, std::mem::take(&mut log.unlogged)))
639            .collect();
640        for (tenant, shed) in unlogged {
641            self.log_shed(tenant, shed);
642        }
643        if timeout_at(deadline.into(), self.writer.wait())
644            .await
645            .is_err()
646        {
647            self.abandon.cancel();
648            self.writer.wait().await;
649        }
650        // With the queue closed and the writer gone nothing else moves these
651        // counts, and admission only happens under the queue lock, so the
652        // admitted bytes are exactly the abandoned items'. The caller that
653        // takes a nonzero `pending` owns its reserved part and settles every
654        // share alone.
655        let lost = self.pending.swap(0, Ordering::AcqRel);
656        if lost == 0 {
657            return 0;
658        }
659        let admission = &self.admission;
660        let reserved = admission.staged_reserved.swap(0, Ordering::AcqRel);
661        let reserved_bytes = admission.reserved_bytes.swap(0, Ordering::AcqRel);
662        let bytes = admission.unreserved_bytes.swap(0, Ordering::AcqRel) + reserved_bytes;
663        admission.release(Load {
664            items: lost,
665            reserved,
666            bytes,
667            reserved_bytes,
668        });
669        count_lost::<S>(lost, LossReason::Shutdown);
670        metrics::gauge!("outbox_pending", "outbox" => S::NAME).decrement(lost as f64);
671        tracing::error!(
672            outbox = S::NAME,
673            lost,
674            "outbox shutdown deadline passed with items unwritten"
675        );
676        self.idle.notify_waiters();
677        lost
678    }
679}
680
681/// Capacity an [`Outbox`] admitted ahead of staging, from [`Outbox::reserve`].
682///
683/// Each [`Reservation::stage`] spends one slot; dropping the reservation
684/// releases the slots it never spent. A value carrying a reservation, such as
685/// a write that hands off to a second outbox, holds that capacity until the
686/// value is staged or dropped.
687pub struct Reservation<S: OutboxSink> {
688    /// The outbox the slots were admitted in.
689    outbox: Arc<Outbox<S>>,
690    /// Slots admitted and not yet staged.
691    slots: AtomicUsize,
692}
693
694impl<S: OutboxSink> Reservation<S> {
695    /// Stages one item for `tenant` on a reserved slot without waiting.
696    ///
697    /// With no slot left, the item is staged like [`Outbox::stage`]. Once
698    /// shutdown has begun the item is counted lost, as any late stage is.
699    pub fn stage(&self, tenant: DataTenantId, item: impl Into<S::Item>) {
700        let spent = self
701            .slots
702            .try_update(Ordering::AcqRel, Ordering::Acquire, |slots| {
703                slots.checked_sub(1)
704            });
705        if spent.is_ok() {
706            self.outbox.send_reserved(tenant, item.into());
707        } else {
708            self.outbox.stage(tenant, item);
709        }
710    }
711}
712
713/// Releases the slots this reservation never spent.
714impl<S: OutboxSink> Drop for Reservation<S> {
715    /// Returns the unspent slots to the reserved share.
716    fn drop(&mut self) {
717        self.outbox
718            .admission
719            .reserved
720            .fetch_sub(*self.slots.get_mut(), Ordering::AcqRel);
721        if *self.slots.get_mut() > 0 {
722            self.outbox.admission.wake();
723        }
724    }
725}
726
727/// Names the outbox and the unspent slots, never the outbox's contents.
728impl<S: OutboxSink> std::fmt::Debug for Reservation<S> {
729    /// Write the sink's name and a relaxed snapshot of the unspent slots.
730    ///
731    /// # Errors
732    /// Returns the formatter's error when it rejects a write.
733    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        formatter
735            .debug_struct("Reservation")
736            .field("outbox", &S::NAME)
737            .field("slots", &self.slots.load(Ordering::Relaxed))
738            .finish()
739    }
740}
741
742/// The two shares an [`Outbox`] admits items and bytes into, shared with its
743/// writer.
744#[derive(Debug, Default)]
745struct Admission {
746    /// Unreserved items not yet written or lost.
747    unreserved: AtomicUsize,
748    /// Unspent reserved slots plus reserved items not yet written or lost.
749    reserved: AtomicUsize,
750    /// Reserved items pending in the writer, which shutdown releases from
751    /// `reserved` when it abandons them.
752    staged_reserved: AtomicUsize,
753    /// Bytes of unreserved items not yet written or lost.
754    unreserved_bytes: AtomicUsize,
755    /// Bytes of reserved items not yet written or lost; unspent slots hold
756    /// none.
757    reserved_bytes: AtomicUsize,
758    /// Signalled whenever items or slots are released while a caller
759    /// waits, waking the first [`Outbox::reserve_until`] waiter.
760    freed: Notify,
761    /// Length of the [`Outbox::reserve_until`] queue; zero lets releases
762    /// skip [`Self::freed`].
763    waiting: AtomicUsize,
764}
765
766impl Admission {
767    /// Releases written or lost items from their shares.
768    fn release(&self, load: Load) {
769        self.reserved.fetch_sub(load.reserved, Ordering::AcqRel);
770        self.unreserved
771            .fetch_sub(load.items - load.reserved, Ordering::AcqRel);
772        self.reserved_bytes
773            .fetch_sub(load.reserved_bytes, Ordering::AcqRel);
774        self.unreserved_bytes
775            .fetch_sub(load.bytes - load.reserved_bytes, Ordering::AcqRel);
776        self.wake();
777    }
778
779    /// Wakes the first [`Outbox::reserve_until`] waiter after a release, if
780    /// any caller waits.
781    ///
782    /// The fence pairs with the one a waiter takes between registering for
783    /// [`Self::freed`] and retrying, so either the waiter sees the released
784    /// capacity or this sees the waiter.
785    fn wake(&self) {
786        fence(Ordering::SeqCst);
787        if self.waiting.load(Ordering::SeqCst) > 0 {
788            self.freed.notify_waiters();
789        }
790    }
791}
792
793/// The [`Outbox::reserve_until`] queue.
794#[derive(Debug, Default)]
795struct Waiters {
796    /// Ticket the next caller to join takes.
797    next: u64,
798    /// Tickets of the waiting callers, head first.
799    queue: VecDeque<u64>,
800    /// The latest refusal of a caller that joined or headed the queue.
801    refused: Option<OutboxFull>,
802}
803
804impl Waiters {
805    /// Adds a caller to the back of the queue and returns its ticket.
806    fn join(&mut self, admission: &Admission) -> u64 {
807        let ticket = self.next;
808        self.next += 1;
809        self.queue.push_back(ticket);
810        admission.waiting.store(self.queue.len(), Ordering::SeqCst);
811        ticket
812    }
813}
814
815/// One caller's place in the [`Outbox::reserve_until`] queue, left when the
816/// caller returns or is cancelled.
817struct Place<'a, S: OutboxSink> {
818    /// The outbox whose queue the caller joined.
819    outbox: &'a Outbox<S>,
820    /// The caller's ticket.
821    ticket: u64,
822}
823
824impl<S: OutboxSink> Drop for Place<'_, S> {
825    /// Leaves the queue and, when this caller was its head, wakes the next
826    /// head to retry.
827    fn drop(&mut self) {
828        let mut waiters = self.outbox.waiters();
829        let head = waiters.queue.front() == Some(&self.ticket);
830        waiters.queue.retain(|ticket| *ticket != self.ticket);
831        let admission = &self.outbox.admission;
832        admission
833            .waiting
834            .store(waiters.queue.len(), Ordering::SeqCst);
835        if waiters.queue.is_empty() {
836            waiters.refused = None;
837        } else if head {
838            admission.freed.notify_waiters();
839        }
840    }
841}
842
843/// What a set of admitted items holds of the [`Admission`] shares.
844#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
845struct Load {
846    /// Items.
847    items: usize,
848    /// Items admitted on reserved slots.
849    reserved: usize,
850    /// Accounted bytes of every item.
851    bytes: usize,
852    /// Accounted bytes of the reserved items.
853    reserved_bytes: usize,
854}
855
856impl Load {
857    /// The load of one item of `bytes`, `reserved` or not.
858    const fn one(reserved: bool, bytes: usize) -> Self {
859        Self {
860            items: 1,
861            reserved: reserved as usize,
862            bytes,
863            reserved_bytes: if reserved { bytes } else { 0 },
864        }
865    }
866
867    /// Adds `other` into this load.
868    const fn add(&mut self, other: Self) {
869        self.items += other.items;
870        self.reserved += other.reserved;
871        self.bytes += other.bytes;
872        self.reserved_bytes += other.reserved_bytes;
873    }
874
875    /// Removes `part`, a load added into this one, from it.
876    const fn sub(&mut self, part: Self) {
877        self.items -= part.items;
878        self.reserved -= part.reserved;
879        self.bytes -= part.bytes;
880        self.reserved_bytes -= part.reserved_bytes;
881    }
882}
883
884/// When a tenant's last shed record was written and how many items it has
885/// shed since.
886#[derive(Debug, Default)]
887struct ShedLog {
888    /// When the last record was written; `None` before the first.
889    logged_at: Option<Instant>,
890    /// Items shed since that record.
891    unlogged: usize,
892}
893
894/// Admits `count` items into `share` unless that takes it past `limit`.
895///
896/// # Errors
897///
898/// Returns the share's count, admitting nothing, when `count` more would
899/// exceed `limit`.
900fn admit(share: &AtomicUsize, count: usize, limit: usize) -> Result<(), usize> {
901    share
902        .try_update(Ordering::AcqRel, Ordering::Acquire, |admitted| {
903            admitted.checked_add(count).filter(|total| *total <= limit)
904        })
905        .map(|_| ())
906}
907
908/// One lane's items in arrival order and their admitted [`Load`].
909type Backlog<I> = (Vec<I>, Load);
910
911/// One lane's received items in arrival order, each with its own admitted
912/// [`Load`] so a bounded prefix can be written, and their total.
913type Waiting<I> = (Vec<(I, Load)>, Load);
914
915/// A tenant and one of its [`OutboxSink::LANES`]: the unit written in arrival
916/// order, one write at a time.
917type Lane = (DataTenantId, usize);
918
919/// Outcome of one spawned lane write: the lane, the slice's key, its items,
920/// and the result, whose error is the sink's error or the message of a
921/// contained panic. The items come back only from a failed write, for its
922/// retry under the same key; a written slice's items are freed off the writer
923/// loop and come back empty.
924type Written<S> = (Lane, Uuid, Vec<<S as OutboxSink>::Item>, Result<(), String>);
925
926/// The writer task behind one [`Outbox`].
927///
928/// Moved into its task by [`Outbox::new`]; it owns the receiving half of the
929/// queue, the per-lane backlogs, the in-flight writes, and the retry state.
930struct OutboxWriter<S: OutboxSink> {
931    /// Destination every write goes to.
932    sink: Arc<S>,
933    /// Receiving half of the queue.
934    requests: mpsc::UnboundedReceiver<Queued<S>>,
935    /// Received items per lane, in arrival order, not being written.
936    waiting: HashMap<Lane, Waiting<S::Item>>,
937    /// When each waiting lane's oldest item arrived, which starts its
938    /// [`OutboxSink::LINGER`].
939    since: HashMap<Lane, Instant>,
940    /// Whether the queue has closed, after which lanes write without
941    /// lingering so shutdown drains promptly.
942    closed: bool,
943    /// The key and exact slice of each lane whose last write failed; it is
944    /// that lane's next write, unmerged with `waiting`.
945    failed: HashMap<Lane, (Uuid, Backlog<S::Item>)>,
946    /// In-flight lane writes.
947    writing: JoinSet<Written<S>>,
948    /// Lane, admitted load, and dispatch time of each in-flight write, keyed
949    /// by its task.
950    in_flight: HashMap<Id, (Lane, Load, Instant)>,
951    /// Earliest retry time and current backoff of each lane whose last
952    /// write failed.
953    retry_at: HashMap<Lane, (Instant, Duration)>,
954    /// Most writes run at once.
955    concurrency: usize,
956    /// Count shared with the handle; decremented once items are written.
957    pending: Arc<AtomicUsize>,
958    /// Admission shares shared with the handle; released with `pending`.
959    admission: Arc<Admission>,
960    /// Woken whenever a settled write leaves nothing pending.
961    idle: Arc<Notify>,
962    /// Cancelled when shutdown reaches its deadline.
963    abandon: CancellationToken,
964}
965
966impl<S: OutboxSink> OutboxWriter<S> {
967    /// The writer loop: receives items, dispatches lane writes, settles
968    /// finished ones, and wakes for retries, until shutdown has dropped the
969    /// sender and nothing is left, or shutdown abandons it at the deadline.
970    ///
971    /// Abandoning returns at once; dropping the writer cancels its in-flight
972    /// writes and discards their items, which shutdown then counts lost.
973    async fn run(mut self) {
974        let mut received = Vec::new();
975        let mut open = true;
976        loop {
977            // One instant for both, so a lane `dispatch` finds still lingering
978            // is one `next_wake` still waits for.
979            let now = Instant::now();
980            self.dispatch(now);
981            if !open && self.waiting.is_empty() && self.failed.is_empty() && self.writing.is_empty()
982            {
983                return;
984            }
985            let retry = self.next_wake(now);
986            tokio::select! {
987                biased;
988                () = self.abandon.cancelled() => return,
989                Some(done) = self.writing.join_next_with_id(), if !self.writing.is_empty() => {
990                    self.finish(done);
991                }
992                count = self.requests.recv_many(&mut received, usize::MAX), if open => {
993                    open = count != 0;
994                    self.closed = !open;
995                    let lanes = S::LANES.max(1) as u64;
996                    let now = Instant::now();
997                    for (tenant, item, load) in received.drain(..) {
998                        // The remainder is below `LANES`, a `usize`.
999                        let lane = usize::try_from(S::lane(&item) % lanes).unwrap_or_default();
1000                        self.since.entry((tenant, lane)).or_insert(now);
1001                        let (items, backlog) = self.waiting.entry((tenant, lane)).or_default();
1002                        items.push((item, load));
1003                        backlog.add(load);
1004                    }
1005                }
1006                () = async {
1007                    match retry {
1008                        Some(at) => sleep_until(at.into()).await,
1009                        None => pending().await,
1010                    }
1011                } => {}
1012            }
1013        }
1014    }
1015
1016    /// Starts one write for each lane that has items waiting, has no write
1017    /// in flight, is not backing off, and has lingered (see
1018    /// [`Self::is_ready`]), while fewer than `concurrency` writes
1019    /// run. Ready lanes are taken in order of their tenant's writes in flight
1020    /// plus the lane's rank among that tenant's ready lanes, so every tenant's
1021    /// first lane goes before any tenant's second. A lane with a failed slice
1022    /// writes exactly that slice again; otherwise a write takes the lane's
1023    /// backlog up to [`OutboxSink::BATCH_BYTES`] (see [`Self::take`]).
1024    ///
1025    /// The write task keeps its items through a panicking sink, including a
1026    /// panic while rendering the sink's error, and returns them with the panic
1027    /// as the error, so they are retried like any failed write.
1028    fn dispatch(&mut self, now: Instant) {
1029        if self.writing.len() >= self.concurrency {
1030            return;
1031        }
1032        let mut busy: HashMap<DataTenantId, usize> = HashMap::new();
1033        for ((tenant, _), ..) in self.in_flight.values() {
1034            *busy.entry(*tenant).or_default() += 1;
1035        }
1036        let mut ready: Vec<(usize, Lane)> = self
1037            .waiting
1038            .keys()
1039            .chain(
1040                self.failed
1041                    .keys()
1042                    .filter(|lane| !self.waiting.contains_key(*lane)),
1043            )
1044            .filter(|lane| self.is_ready(**lane, now))
1045            .map(|lane| {
1046                let order = busy.entry(lane.0).or_default();
1047                *order += 1;
1048                (*order, *lane)
1049            })
1050            .collect();
1051        ready.sort_unstable_by_key(|(order, _)| *order);
1052        for (_, lane) in ready {
1053            if self.writing.len() >= self.concurrency {
1054                return;
1055            }
1056            let Some((slice, (items, load))) = self
1057                .failed
1058                .remove(&lane)
1059                .or_else(|| self.take(lane).map(|backlog| (Uuid::now_v7(), backlog)))
1060            else {
1061                continue;
1062            };
1063            let (tenant, _) = lane;
1064            let sink = Arc::clone(&self.sink);
1065            let task = self.writing.spawn(async move {
1066                // The constructing closure borrows `items`, so it is dropped
1067                // before awaiting, and the write before `items` is returned.
1068                let outcome = {
1069                    let write =
1070                        catch_unwind(AssertUnwindSafe(|| sink.write(tenant, slice, &items)));
1071                    match write {
1072                        Ok(write) => contain_panic(write).await,
1073                        Err(panic) => Err(panic),
1074                    }
1075                };
1076                // Rendering a sink error runs sink code too, so it is
1077                // contained as well and `items` is still returned.
1078                let result = catch_unwind(AssertUnwindSafe(|| match outcome {
1079                    Ok(written) => written.map_err(|error| error.to_string()),
1080                    Err(panic) => Err(format!("sink write panicked: {}", panic_message(&*panic))),
1081                }))
1082                .unwrap_or_else(|panic| {
1083                    Err(format!(
1084                        "sink error rendering panicked: {}",
1085                        panic_message(&*panic)
1086                    ))
1087                });
1088                if result.is_ok() {
1089                    // A large slice's items can take as long to free as to
1090                    // write; freeing them on the blocking pool keeps the
1091                    // writer loop free to receive and dispatch the next write.
1092                    drop(tokio::task::spawn_blocking(move || drop(items)));
1093                    return (lane, slice, Vec::new(), result);
1094                }
1095                (lane, slice, items, result)
1096            });
1097            self.in_flight
1098                .insert(task.id(), (lane, load, Instant::now()));
1099        }
1100    }
1101
1102    /// Removes the next slice of `lane`'s backlog: the longest prefix within
1103    /// [`OutboxSink::BATCH_BYTES`], and at least its first item. A remainder
1104    /// keeps its place and the lane's linger start, so it is ready to write
1105    /// as soon as this slice is written.
1106    fn take(&mut self, lane: Lane) -> Option<Backlog<S::Item>> {
1107        let (queued, total) = self.waiting.get_mut(&lane)?;
1108        let mut load = Load::default();
1109        let count = queued
1110            .iter()
1111            .position(|(_, next)| {
1112                let over = load.items > 0 && load.bytes.saturating_add(next.bytes) > S::BATCH_BYTES;
1113                if !over {
1114                    load.add(*next);
1115                }
1116                over
1117            })
1118            .unwrap_or(queued.len());
1119        let items = if count == queued.len() {
1120            self.since.remove(&lane);
1121            self.waiting.remove(&lane)?.0
1122        } else {
1123            total.sub(load);
1124            queued.drain(..count).collect()
1125        };
1126        Some((items.into_iter().map(|(item, _)| item).collect(), load))
1127    }
1128
1129    /// Whether `lane` has no write in flight, its backoff has elapsed, and
1130    /// its next write may go now: a failed slice retries at once, and a
1131    /// backlog goes once its oldest item has lingered [`OutboxSink::LINGER`],
1132    /// it holds [`OutboxSink::BATCH_ITEMS`] items or
1133    /// [`OutboxSink::BATCH_BYTES`] bytes, or the queue has closed.
1134    fn is_ready(&self, lane: Lane, now: Instant) -> bool {
1135        !self.in_flight.values().any(|(busy, ..)| *busy == lane)
1136            && self.retry_at.get(&lane).is_none_or(|(at, _)| *at <= now)
1137            && (self.closed || self.failed.contains_key(&lane) || self.lingered(lane, now))
1138    }
1139
1140    /// Whether `lane`'s backlog has waited [`OutboxSink::LINGER`] or holds
1141    /// [`OutboxSink::BATCH_ITEMS`] items or [`OutboxSink::BATCH_BYTES`] bytes.
1142    fn lingered(&self, lane: Lane, now: Instant) -> bool {
1143        self.linger_until(lane).is_none_or(|at| at <= now)
1144            || self.waiting.get(&lane).is_some_and(|(items, load)| {
1145                items.len() >= S::BATCH_ITEMS || load.bytes >= S::BATCH_BYTES
1146            })
1147    }
1148
1149    /// When `lane`'s oldest waiting item has lingered, or `None` when it has
1150    /// nothing waiting.
1151    fn linger_until(&self, lane: Lane) -> Option<Instant> {
1152        self.since.get(&lane).map(|since| *since + S::LINGER)
1153    }
1154
1155    /// The earliest time after `now` a failed lane may retry or a lingering
1156    /// lane may write, or `None` when no timer is needed because every write
1157    /// slot is busy or nothing waits on time.
1158    ///
1159    /// `now` must be the instant the preceding [`Self::dispatch`] judged
1160    /// readiness at: a lane it left lingering then has its deadline after
1161    /// `now`, so the writer always wakes for it.
1162    fn next_wake(&self, now: Instant) -> Option<Instant> {
1163        if self.writing.len() >= self.concurrency {
1164            return None;
1165        }
1166        let retries = self
1167            .retry_at
1168            .iter()
1169            .filter(|(lane, (at, _))| *at > now && self.failed.contains_key(*lane))
1170            .map(|(_, (at, _))| *at);
1171        let lingering = self
1172            .since
1173            .keys()
1174            .filter_map(|lane| self.linger_until(*lane))
1175            .filter(|at| *at > now);
1176        retries.chain(lingering).min()
1177    }
1178
1179    /// Settles one finished write.
1180    ///
1181    /// Every joined write, succeeded or failed, records its dispatch-to-settle
1182    /// time in `outbox_write_seconds`. Success clears the lane's backoff,
1183    /// counts the items and bytes written, and releases the slots of the
1184    /// items it wrote, which the write task already handed to the blocking
1185    /// pool to free. Failure,
1186    /// including a contained sink panic, keeps the items as the lane's next
1187    /// write, unmerged with later arrivals, doubles its backoff, and counts
1188    /// and logs the attempt. A task that failed to join lost its items, which
1189    /// are counted lost with [`LossReason::Rejected`]; with sink panics
1190    /// contained that needs a panic that escapes containment, such as one
1191    /// raised while dropping a panic payload.
1192    fn finish(&mut self, done: Result<(Id, Written<S>), JoinError>) {
1193        let ((lane, slice, items, result), load) = match done {
1194            Ok((id, written)) => {
1195                let load =
1196                    self.in_flight
1197                        .remove(&id)
1198                        .map_or_else(Load::default, |(_, load, started)| {
1199                            metrics::histogram!("outbox_write_seconds", "outbox" => S::NAME)
1200                                .record(started.elapsed().as_secs_f64());
1201                            load
1202                        });
1203                (written, load)
1204            }
1205            Err(error) => {
1206                if let Some(((tenant, lane), load, _)) = self.in_flight.remove(&error.id()) {
1207                    count_lost::<S>(load.items, LossReason::Rejected);
1208                    tracing::error!(outbox = S::NAME, %tenant, lane, %error, lost = load.items, "outbox write task failed");
1209                    self.release(load);
1210                }
1211                return;
1212            }
1213        };
1214        let error = match result {
1215            Ok(()) => {
1216                self.retry_at.remove(&lane);
1217                metrics::counter!("outbox_items_written_total", "outbox" => S::NAME)
1218                    .increment(load.items as u64);
1219                metrics::counter!("outbox_bytes_written_total", "outbox" => S::NAME)
1220                    .increment(load.bytes as u64);
1221                self.release(load);
1222                return;
1223            }
1224            Err(error) => error,
1225        };
1226        let backoff = self
1227            .retry_at
1228            .get(&lane)
1229            .map_or(INITIAL_BACKOFF, |(_, last)| (*last * 2).min(MAX_BACKOFF));
1230        self.retry_at
1231            .insert(lane, (Instant::now() + backoff, backoff));
1232        metrics::counter!("outbox_write_failures_total", "outbox" => S::NAME).increment(1);
1233        tracing::error!(
1234            outbox = S::NAME,
1235            tenant = %lane.0,
1236            lane = lane.1,
1237            %error,
1238            items = items.len(),
1239            retry_in_ms = backoff.as_millis(),
1240            "outbox write failed; retrying"
1241        );
1242        self.failed.insert(lane, (slice, (items, load)));
1243    }
1244
1245    /// Releases written or lost items from pending and their shares, and
1246    /// wakes settlers when nothing remains pending.
1247    fn release(&self, load: Load) {
1248        metrics::gauge!("outbox_pending", "outbox" => S::NAME).decrement(load.items as f64);
1249        self.admission
1250            .staged_reserved
1251            .fetch_sub(load.reserved, Ordering::AcqRel);
1252        self.admission.release(load);
1253        if self.pending.fetch_sub(load.items, Ordering::AcqRel) == load.items {
1254            self.idle.notify_waiters();
1255        }
1256    }
1257}
1258
1259/// Polls `future` to completion and returns its output, or the payload of a
1260/// panic raised while polling it.
1261///
1262/// Containing the panic inside the polled task, rather than letting it unwind
1263/// the task, keeps the caller's owned state, such as a write's items, intact.
1264///
1265/// # Errors
1266///
1267/// Returns the panic payload when polling `future` panics; the future is then
1268/// dropped without being polled again.
1269async fn contain_panic<F: Future>(future: F) -> Result<F::Output, Box<dyn Any + Send>> {
1270    let mut future = pin!(future);
1271    poll_fn(
1272        |context| match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(context))) {
1273            Ok(poll) => poll.map(Ok),
1274            Err(panic) => Poll::Ready(Err(panic)),
1275        },
1276    )
1277    .await
1278}
1279
1280/// The message of a panic payload, when it is a string.
1281fn panic_message(panic: &(dyn Any + Send)) -> &str {
1282    panic
1283        .downcast_ref::<&str>()
1284        .copied()
1285        .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
1286        .unwrap_or("non-string panic payload")
1287}
1288
1289/// Counts `count` items of outbox `S` as lost in
1290/// `outbox_events_lost_total{outbox, reason}`.
1291///
1292/// The outbox counts shed, shutdown, and late-staging loss itself; a sink
1293/// calls this for items it consumes without writing, such as a terminal
1294/// rejection that a retry could never cure, and a producer for items it
1295/// sheds at its own limit before staging.
1296pub fn count_lost<S: OutboxSink>(count: usize, reason: LossReason) {
1297    metrics::counter!("outbox_events_lost_total", "outbox" => S::NAME, "reason" => reason.as_str())
1298        .increment(count as u64);
1299}
1300
1301/// Counts and logs one item of `tenant` lost because it was staged after
1302/// shutdown began or the writer is gone.
1303fn lose_late<S: OutboxSink>(tenant: DataTenantId) {
1304    count_lost::<S>(1, LossReason::Shutdown);
1305    tracing::error!(outbox = S::NAME, %tenant, "outbox item staged after shutdown was lost");
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310    //! The generic outbox against an in-memory sink whose tenants can be made
1311    //! to fail on demand.
1312
1313    use std::collections::{HashMap, HashSet};
1314    use std::fmt::{self, Display, Formatter};
1315    use std::future::{Future, poll_fn};
1316    use std::pin::pin;
1317    use std::sync::atomic::Ordering;
1318    use std::sync::{Arc, Mutex, MutexGuard};
1319    use std::task::Poll;
1320    use std::time::{Duration, Instant};
1321
1322    use metrics::atomics::AtomicU64;
1323    use metrics::{
1324        Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit,
1325    };
1326    use tokio::sync::Notify;
1327    use uuid::Uuid;
1328    use wyrd_spec::DataTenantId;
1329
1330    use super::{Outbox, OutboxLimits, OutboxSink};
1331
1332    /// In-memory sink recording each tenant's written items in order.
1333    #[derive(Default)]
1334    struct MemorySink {
1335        /// Shared record of written items and failing tenants.
1336        state: Arc<Mutex<MemoryState>>,
1337        /// Writes of hanging tenants block until notified, simulating a hang.
1338        hang: Arc<Notify>,
1339        /// Notified as each write starts, so a test can wait for a dispatch
1340        /// without polling.
1341        started: Arc<Notify>,
1342    }
1343
1344    /// What the memory sink has written and which tenants fail or hang.
1345    #[derive(Default)]
1346    struct MemoryState {
1347        /// Written items per tenant, in write order.
1348        written: HashMap<DataTenantId, Vec<u32>>,
1349        /// Tenants whose writes fail.
1350        failing: HashSet<DataTenantId>,
1351        /// Tenants whose writes hang.
1352        hanging: HashSet<DataTenantId>,
1353        /// Tenants whose next write panics once.
1354        panicking: HashSet<DataTenantId>,
1355        /// Tenants whose next write fails once with an error whose `Display`
1356        /// panics.
1357        display_panicking: HashSet<DataTenantId>,
1358        /// Failed write attempts.
1359        failures: usize,
1360        /// Every write attempt's tenant, slice key, and items, in attempt
1361        /// order.
1362        attempts: Vec<(DataTenantId, Uuid, Vec<u32>)>,
1363    }
1364
1365    /// Failure the memory sink reports for an injected write failure.
1366    #[derive(Debug)]
1367    struct MemoryError {
1368        /// Whether rendering this error panics, simulating a sink error type
1369        /// whose `Display` implementation is faulty.
1370        panics_on_display: bool,
1371    }
1372
1373    impl Display for MemoryError {
1374        /// Renders the injected failure message.
1375        ///
1376        /// # Panics
1377        ///
1378        /// Panics when `panics_on_display` is set, so a test can prove the
1379        /// writer contains a panic raised while rendering a sink error.
1380        fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1381            assert!(!self.panics_on_display, "injected error display panic");
1382            formatter.write_str("injected failure")
1383        }
1384    }
1385
1386    impl OutboxSink for MemorySink {
1387        type Item = u32;
1388        type Error = MemoryError;
1389        const NAME: &'static str = "test";
1390
1391        /// Accounts each item as its own value in bytes, so a test sizes
1392        /// items by choosing their values.
1393        fn weight(item: &u32) -> usize {
1394            *item as usize
1395        }
1396
1397        /// Writes `items` for `tenant`, applying the injections configured in
1398        /// [`MemoryState`] in a fixed order the tests depend on.
1399        ///
1400        /// The order is: notify `started` that a write was dispatched; if the
1401        /// tenant is hanging, wait for `hang`; if the tenant has a one-shot
1402        /// panic injected, consume it and panic; if the tenant has a one-shot
1403        /// panicking-display failure injected, consume it, count a failure,
1404        /// and fail; if the tenant is failing, count a failure and fail;
1405        /// otherwise append `items` to the tenant's written record.
1406        ///
1407        /// # Errors
1408        ///
1409        /// Returns a [`MemoryError`] when the tenant is failing or has a
1410        /// panicking-display failure injected; that error's `Display` panics
1411        /// only in the latter case.
1412        ///
1413        /// # Panics
1414        ///
1415        /// Panics once, after the hang stage, when a panic is injected for the
1416        /// tenant.
1417        ///
1418        /// # Cancellation
1419        ///
1420        /// Dropping the future while it waits for `hang` leaves the state
1421        /// unchanged, so the batch is neither written nor counted as failed.
1422        async fn write(
1423            &self,
1424            tenant: DataTenantId,
1425            slice: Uuid,
1426            items: &[u32],
1427        ) -> Result<(), MemoryError> {
1428            self.started.notify_one();
1429            let hangs = self.lock().hanging.contains(&tenant);
1430            if hangs {
1431                self.hang.notified().await;
1432            }
1433            let panics = self.lock().panicking.remove(&tenant);
1434            assert!(!panics, "injected sink panic");
1435            let mut state = self.lock();
1436            state.attempts.push((tenant, slice, items.to_vec()));
1437            if state.display_panicking.remove(&tenant) {
1438                state.failures += 1;
1439                return Err(MemoryError {
1440                    panics_on_display: true,
1441                });
1442            }
1443            if state.failing.contains(&tenant) {
1444                state.failures += 1;
1445                return Err(MemoryError {
1446                    panics_on_display: false,
1447                });
1448            }
1449            state
1450                .written
1451                .entry(tenant)
1452                .or_default()
1453                .extend_from_slice(items);
1454            Ok(())
1455        }
1456    }
1457
1458    impl MemorySink {
1459        /// Locks the shared state, which no test holds across an await.
1460        fn lock(&self) -> MutexGuard<'_, MemoryState> {
1461            self.state
1462                .lock()
1463                .expect("memory sink state is never poisoned")
1464        }
1465    }
1466
1467    /// An item whose drop blocks until its gate opens or ten seconds pass,
1468    /// standing in for a slice that is slow to free.
1469    struct SlowDrop(Mutex<std::sync::mpsc::Receiver<()>>);
1470
1471    impl Drop for SlowDrop {
1472        /// Blocks until the gate opens, its sender is dropped, or ten
1473        /// seconds pass.
1474        fn drop(&mut self) {
1475            if let Ok(gate) = self.0.lock() {
1476                let _ = gate.recv_timeout(Duration::from_secs(10));
1477            }
1478        }
1479    }
1480
1481    /// A sink that accepts every write of [`SlowDrop`] items.
1482    struct SlowDropSink;
1483
1484    impl OutboxSink for SlowDropSink {
1485        type Item = SlowDrop;
1486        type Error = MemoryError;
1487        const NAME: &'static str = "slow_drop";
1488
1489        /// Accepts `items` without inspecting them.
1490        ///
1491        /// # Errors
1492        ///
1493        /// Never fails.
1494        fn write(
1495            &self,
1496            _tenant: DataTenantId,
1497            _slice: Uuid,
1498            _items: &[SlowDrop],
1499        ) -> impl Future<Output = Result<(), MemoryError>> + Send {
1500            std::future::ready(Ok(()))
1501        }
1502    }
1503
1504    /// Proves freeing a written slice does not hold up the writer: while the
1505    /// first slice's item is still blocked in its drop, a slice staged after
1506    /// it is written and settles.
1507    ///
1508    /// # Panics
1509    ///
1510    /// Panics when the second slice waits for the first slice's drop. Two
1511    /// workers keep a blocked writer loop from also blocking the test.
1512    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1513    async fn freeing_a_written_slice_does_not_hold_up_the_next_write() {
1514        let outbox = Outbox::new(SlowDropSink, 1, ROOMY);
1515        let tenant = DataTenantId::new_v7();
1516        let (gate, held) = std::sync::mpsc::channel();
1517        outbox.stage(tenant, SlowDrop(Mutex::new(held)));
1518        let first = outbox.settle(Instant::now() + Duration::from_secs(2)).await;
1519        let (_, open) = std::sync::mpsc::channel();
1520        outbox.stage(tenant, SlowDrop(Mutex::new(open)));
1521        let second = outbox.settle(Instant::now() + Duration::from_secs(2)).await;
1522        drop(gate);
1523        assert_eq!((first, second), (0, 0), "the next write waited for a drop");
1524    }
1525
1526    /// A sink that lingers [`LingerSink::LINGER`] and writes at three items,
1527    /// recording each written slice.
1528    #[derive(Default)]
1529    struct LingerSink {
1530        /// Every written slice, in completion order.
1531        written: Mutex<Vec<Vec<u32>>>,
1532    }
1533
1534    impl OutboxSink for LingerSink {
1535        type Item = u32;
1536        type Error = MemoryError;
1537        const NAME: &'static str = "linger";
1538        const LINGER: Duration = Duration::from_millis(300);
1539        const BATCH_ITEMS: usize = 3;
1540
1541        /// Records `items` once written.
1542        ///
1543        /// # Errors
1544        ///
1545        /// Never fails.
1546        ///
1547        /// # Panics
1548        ///
1549        /// Panics if the record's lock is poisoned.
1550        fn write(
1551            &self,
1552            _tenant: DataTenantId,
1553            _slice: Uuid,
1554            items: &[u32],
1555        ) -> impl Future<Output = Result<(), MemoryError>> + Send {
1556            self.written
1557                .lock()
1558                .expect("unpoisoned")
1559                .push(items.to_vec());
1560            std::future::ready(Ok(()))
1561        }
1562    }
1563
1564    /// Proves a lingering lane writes items staged one at a time as one
1565    /// write once its oldest has lingered, and writes at once on reaching
1566    /// `BATCH_ITEMS`.
1567    ///
1568    /// # Panics
1569    ///
1570    /// Panics when the trickled items are split across writes, a full batch
1571    /// waits out the linger, or the outbox does not settle.
1572    #[tokio::test]
1573    async fn a_lingering_lane_batches_trickled_items_and_writes_a_full_batch_at_once() {
1574        let outbox = Outbox::new(LingerSink::default(), 1, ROOMY);
1575        let tenant = DataTenantId::new_v7();
1576        outbox.stage(tenant, 1_u32);
1577        outbox.stage(tenant, 2_u32);
1578        assert_eq!(
1579            outbox.settle(within(5)).await,
1580            0,
1581            "the lingering lane settles"
1582        );
1583        let started = Instant::now();
1584        for item in 3_u32..=5 {
1585            outbox.stage(tenant, item);
1586        }
1587        assert_eq!(outbox.settle(within(5)).await, 0, "the full batch settles");
1588        assert!(
1589            started.elapsed() < LingerSink::LINGER,
1590            "a full batch waited out the linger"
1591        );
1592        let written = outbox.sink().written.lock().expect("unpoisoned").clone();
1593        assert_eq!(written, [vec![1, 2], vec![3, 4, 5]]);
1594    }
1595
1596    /// Proves the writer judges a lane's readiness and its next wake at one
1597    /// instant: a lane one tick short of its linger is left waiting with a
1598    /// wake at its deadline, and at that deadline it is written.
1599    ///
1600    /// # Panics
1601    ///
1602    /// Panics when the short lane is written, has no wake at its deadline,
1603    /// or is not written at it, or the monotonic clock is not yet one linger
1604    /// past its origin.
1605    #[tokio::test]
1606    async fn a_lane_short_of_its_linger_keeps_a_wake_at_its_deadline() {
1607        let (_queue, requests) = tokio::sync::mpsc::unbounded_channel();
1608        let mut writer = super::OutboxWriter {
1609            sink: Arc::new(LingerSink::default()),
1610            requests,
1611            waiting: HashMap::new(),
1612            since: HashMap::new(),
1613            closed: false,
1614            failed: HashMap::new(),
1615            writing: tokio::task::JoinSet::new(),
1616            in_flight: HashMap::new(),
1617            retry_at: HashMap::new(),
1618            concurrency: 1,
1619            pending: Arc::default(),
1620            admission: Arc::default(),
1621            idle: Arc::default(),
1622            abandon: tokio_util::sync::CancellationToken::new(),
1623        };
1624        let lane = (DataTenantId::new_v7(), 0);
1625        // The deadline has already passed on the clock, as it can between
1626        // the writer's dispatch and its wake.
1627        let since = Instant::now()
1628            .checked_sub(LingerSink::LINGER + Duration::from_secs(1))
1629            .expect("the monotonic clock is past one linger");
1630        writer.since.insert(lane, since);
1631        writer
1632            .waiting
1633            .entry(lane)
1634            .or_default()
1635            .0
1636            .push((1_u32, super::Load::default()));
1637        let deadline = since + LingerSink::LINGER;
1638        let short = deadline
1639            .checked_sub(Duration::from_nanos(1))
1640            .expect("the deadline is past one tick");
1641        writer.dispatch(short);
1642        assert!(writer.writing.is_empty(), "a lingering lane was written");
1643        assert_eq!(
1644            writer.next_wake(short),
1645            Some(deadline),
1646            "a lingering lane has no wake at its deadline"
1647        );
1648        writer.dispatch(deadline);
1649        assert_eq!(
1650            writer.writing.len(),
1651            1,
1652            "the lane is written at its deadline"
1653        );
1654    }
1655
1656    /// A two-lane sink, odd items in lane 1 and even in lane 0, that records
1657    /// each written slice and holds the slice `[0]` until `gate` is notified.
1658    #[derive(Default)]
1659    struct LaneSink {
1660        /// Releases the held `[0]` write.
1661        gate: Notify,
1662        /// Every written slice, in completion order.
1663        written: Mutex<Vec<Vec<u32>>>,
1664    }
1665
1666    impl OutboxSink for LaneSink {
1667        type Item = u32;
1668        type Error = MemoryError;
1669        const NAME: &'static str = "lanes";
1670        const LANES: usize = 2;
1671
1672        /// An item's parity is its lane.
1673        fn lane(item: &u32) -> u64 {
1674            u64::from(item % 2)
1675        }
1676
1677        /// Records `items` once written, first waiting for `gate` when the
1678        /// slice is exactly `[0]`.
1679        ///
1680        /// # Errors
1681        ///
1682        /// Never fails.
1683        ///
1684        /// # Panics
1685        ///
1686        /// Panics if the record's lock is poisoned.
1687        async fn write(
1688            &self,
1689            _tenant: DataTenantId,
1690            _slice: Uuid,
1691            items: &[u32],
1692        ) -> Result<(), MemoryError> {
1693            if items == [0] {
1694                self.gate.notified().await;
1695            }
1696            self.written
1697                .lock()
1698                .expect("unpoisoned")
1699                .push(items.to_vec());
1700            Ok(())
1701        }
1702    }
1703
1704    /// One tenant's lanes write concurrently while each lane keeps arrival
1705    /// order: lane 1 commits while lane 0's first write hangs, and lane 0's
1706    /// later items wait for that write instead of passing it.
1707    ///
1708    /// # Panics
1709    ///
1710    /// Panics when lane 1 waits for lane 0, or lane 0 writes out of order.
1711    #[tokio::test]
1712    async fn a_tenants_lanes_write_concurrently_and_each_keeps_order() {
1713        let outbox = Outbox::new(LaneSink::default(), 2, ROOMY);
1714        let tenant = DataTenantId::new_v7();
1715        outbox.stage(tenant, 0_u32);
1716        outbox.stage(tenant, 1_u32);
1717        let deadline = within(10);
1718        while outbox.sink().written.lock().expect("unpoisoned").is_empty() {
1719            assert!(Instant::now() < deadline, "lane 1 waited for lane 0");
1720            tokio::time::sleep(Duration::from_millis(5)).await;
1721        }
1722        outbox.stage(tenant, 2_u32);
1723        outbox.stage(tenant, 4_u32);
1724        outbox.sink().gate.notify_one();
1725
1726        assert_eq!(outbox.settle(within(10)).await, 0);
1727        assert_eq!(
1728            *outbox.sink().written.lock().expect("unpoisoned"),
1729            vec![vec![1], vec![0], vec![2, 4]]
1730        );
1731    }
1732
1733    /// A sink whose items weigh their value, written at most
1734    /// [`ByteSink::BATCH_BYTES`] at a time, that records each written slice
1735    /// and holds the slice `[0]` until `gate` is notified.
1736    #[derive(Default)]
1737    struct ByteSink {
1738        /// Notified when the held `[0]` write starts.
1739        started: Notify,
1740        /// Releases the held `[0]` write.
1741        gate: Notify,
1742        /// Every written slice, in completion order.
1743        written: Mutex<Vec<Vec<u32>>>,
1744    }
1745
1746    impl OutboxSink for ByteSink {
1747        type Item = u32;
1748        type Error = MemoryError;
1749        const NAME: &'static str = "bytes";
1750        const BATCH_BYTES: usize = 10;
1751
1752        /// An item weighs its value.
1753        fn weight(item: &u32) -> usize {
1754            *item as usize
1755        }
1756
1757        /// Records `items` once written, first waiting for `gate` when the
1758        /// slice is exactly `[0]`.
1759        ///
1760        /// # Errors
1761        ///
1762        /// Never fails.
1763        ///
1764        /// # Panics
1765        ///
1766        /// Panics if the record's lock is poisoned.
1767        async fn write(
1768            &self,
1769            _tenant: DataTenantId,
1770            _slice: Uuid,
1771            items: &[u32],
1772        ) -> Result<(), MemoryError> {
1773            if items == [0] {
1774                self.started.notify_one();
1775                self.gate.notified().await;
1776            }
1777            self.written
1778                .lock()
1779                .expect("unpoisoned")
1780                .push(items.to_vec());
1781            Ok(())
1782        }
1783    }
1784
1785    /// A backlog heavier than `BATCH_BYTES` is written in arrival order as
1786    /// consecutive slices of at most that many bytes, an item heavier than
1787    /// the bound alone.
1788    ///
1789    /// # Panics
1790    ///
1791    /// Panics when a slice exceeds the bound with more than one item, items
1792    /// leave arrival order, or the outbox does not settle.
1793    #[tokio::test]
1794    async fn a_heavy_backlog_is_written_in_bounded_slices_in_order() {
1795        let outbox = Outbox::new(ByteSink::default(), 1, ROOMY);
1796        let tenant = DataTenantId::new_v7();
1797        outbox.stage(tenant, 0_u32);
1798        outbox.sink().started.notified().await;
1799        for item in [4_u32, 5, 1, 20, 3] {
1800            outbox.stage(tenant, item);
1801        }
1802        outbox.sink().gate.notify_one();
1803
1804        assert_eq!(outbox.settle(within(10)).await, 0);
1805        assert_eq!(
1806            *outbox.sink().written.lock().expect("unpoisoned"),
1807            vec![vec![0], vec![4, 5, 1], vec![20], vec![3]]
1808        );
1809    }
1810
1811    /// Capacity of test outboxes that do not exercise the bound.
1812    const ROOMY: OutboxLimits = items(1_024);
1813
1814    /// Limits of `items` items and unlimited bytes.
1815    const fn items(items: usize) -> OutboxLimits {
1816        OutboxLimits {
1817            items,
1818            bytes: usize::MAX,
1819        }
1820    }
1821
1822    /// Starts an outbox over a fresh memory sink and returns both.
1823    fn outbox(concurrency: usize) -> (Arc<Outbox<MemorySink>>, Arc<Mutex<MemoryState>>) {
1824        let sink = MemorySink::default();
1825        let state = Arc::clone(&sink.state);
1826        (Outbox::new(sink, concurrency, ROOMY), state)
1827    }
1828
1829    /// Starts an outbox over `sink` and returns it with the sink's state and
1830    /// write-start signal.
1831    fn outbox_over(
1832        sink: MemorySink,
1833    ) -> (
1834        Arc<Outbox<MemorySink>>,
1835        Arc<Mutex<MemoryState>>,
1836        Arc<Notify>,
1837    ) {
1838        let state = Arc::clone(&sink.state);
1839        let started = Arc::clone(&sink.started);
1840        (Outbox::new(sink, 4, ROOMY), state, started)
1841    }
1842
1843    /// Thread-local metrics recorder keeping every counter and gauge by name,
1844    /// and by name and `reason` label when it has one.
1845    ///
1846    /// Installed with `metrics::set_default_local_recorder` in a
1847    /// current-thread test, it also sees the outbox writer's updates because
1848    /// every task runs on the test thread.
1849    #[derive(Default)]
1850    struct TestMetrics {
1851        /// Raw value of each metric, keyed `name` or `name{reason=...}`;
1852        /// gauges store `f64` bits.
1853        values: Mutex<HashMap<String, Arc<AtomicU64>>>,
1854    }
1855
1856    impl TestMetrics {
1857        /// The shared value cell of the metric `key` names, one per `reason`
1858        /// label value.
1859        fn cell(&self, key: &Key) -> Arc<AtomicU64> {
1860            let name = key
1861                .labels()
1862                .find(|label| label.key() == "reason")
1863                .map_or_else(
1864                    || key.name().to_owned(),
1865                    |reason| format!("{}{{reason={}}}", key.name(), reason.value()),
1866                );
1867            let mut values = self.values.lock().expect("test metrics are never poisoned");
1868            Arc::clone(values.entry(name).or_default())
1869        }
1870
1871        /// The raw value of metric `name`, summed over its `reason` labels
1872        /// unless `name` names one as `name{reason=...}`; zero when it was
1873        /// never touched.
1874        fn raw(&self, name: &str) -> u64 {
1875            let labelled = format!("{name}{{");
1876            self.values
1877                .lock()
1878                .expect("test metrics are never poisoned")
1879                .iter()
1880                .filter(|(key, _)| *key == name || key.starts_with(&labelled))
1881                .map(|(_, value)| value.load(Ordering::Acquire))
1882                .sum()
1883        }
1884
1885        /// The value of counter `name`.
1886        fn counter(&self, name: &str) -> u64 {
1887            self.raw(name)
1888        }
1889
1890        /// The value of gauge `name`.
1891        fn gauge(&self, name: &str) -> f64 {
1892            f64::from_bits(self.raw(name))
1893        }
1894    }
1895
1896    impl Recorder for TestMetrics {
1897        /// Intentionally ignores counter descriptions; tests assert values
1898        /// only.
1899        fn describe_counter(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
1900
1901        /// Intentionally ignores gauge descriptions; tests assert values only.
1902        fn describe_gauge(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
1903
1904        /// Intentionally ignores histogram descriptions; histograms are not
1905        /// recorded.
1906        fn describe_histogram(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
1907
1908        /// Records the counter by metric name, ignoring labels, so every
1909        /// registration of one name shares a single value cell.
1910        fn register_counter(&self, key: &Key, _: &Metadata<'_>) -> Counter {
1911            Counter::from_arc(self.cell(key))
1912        }
1913
1914        /// Records the gauge by metric name, ignoring labels, storing its
1915        /// `f64` value as bits in a shared cell.
1916        fn register_gauge(&self, key: &Key, _: &Metadata<'_>) -> Gauge {
1917            Gauge::from_arc(self.cell(key))
1918        }
1919
1920        /// Intentionally records nothing: the outbox emits no histogram the
1921        /// tests assert on.
1922        fn register_histogram(&self, _: &Key, _: &Metadata<'_>) -> Histogram {
1923            Histogram::noop()
1924        }
1925    }
1926
1927    /// A deadline `seconds` from now.
1928    fn within(seconds: u64) -> Instant {
1929        Instant::now() + Duration::from_secs(seconds)
1930    }
1931
1932    /// Waits until the sink has recorded at least `failures` failed writes.
1933    async fn await_failures(state: &Mutex<MemoryState>, failures: usize) {
1934        let deadline = within(10);
1935        while state.lock().expect("unpoisoned").failures < failures {
1936            assert!(Instant::now() < deadline, "the write never failed");
1937            tokio::time::sleep(Duration::from_millis(5)).await;
1938        }
1939    }
1940
1941    /// A failed write is retried and committed exactly once, in order, ahead of
1942    /// the tenant's later items, and `pending` returns to zero.
1943    #[tokio::test]
1944    async fn a_failed_write_is_retried_once_in_order_ahead_of_later_items() {
1945        let (outbox, state) = outbox(4);
1946        let tenant = DataTenantId::new_v7();
1947        state.lock().expect("unpoisoned").failing.insert(tenant);
1948        outbox.stage(tenant, 1_u32);
1949        outbox.stage(tenant, 2_u32);
1950        await_failures(&state, 2).await;
1951        outbox.stage(tenant, 3_u32);
1952        assert_eq!(outbox.pending(), 3, "failed items stay pending");
1953        state.lock().expect("unpoisoned").failing.clear();
1954
1955        assert_eq!(outbox.settle(within(10)).await, 0, "the retry drains");
1956        assert_eq!(
1957            state.lock().expect("unpoisoned").written.get(&tenant),
1958            Some(&vec![1, 2, 3]),
1959            "retried items commit once, ahead of later items"
1960        );
1961        assert_eq!(outbox.pending(), 0);
1962    }
1963
1964    /// A retry writes exactly the failed slice under its key: every attempt
1965    /// sees the same items and key, and an item staged after the failure is
1966    /// written separately under a new key once the retry succeeds, so a sink
1967    /// deriving identity from the key presents one identity across attempts.
1968    #[tokio::test]
1969    async fn a_retry_writes_the_identical_failed_slice_without_later_items() {
1970        let (outbox, state) = outbox(4);
1971        let tenant = DataTenantId::new_v7();
1972        state.lock().expect("unpoisoned").failing.insert(tenant);
1973        outbox.stage(tenant, 1_u32);
1974        outbox.stage(tenant, 2_u32);
1975        await_failures(&state, 1).await;
1976        outbox.stage(tenant, 3_u32);
1977        await_failures(&state, 2).await;
1978        state.lock().expect("unpoisoned").failing.clear();
1979
1980        assert_eq!(outbox.settle(within(10)).await, 0, "the retry drains");
1981        let state = state.lock().expect("unpoisoned");
1982        let (last, retries) = state
1983            .attempts
1984            .split_last()
1985            .expect("the later item was written");
1986        assert_eq!(
1987            (last.0, &last.2),
1988            (tenant, &vec![3]),
1989            "the later item is its own write"
1990        );
1991        assert!(retries.len() >= 3, "two failures and one successful retry");
1992        let slice = retries[0].1;
1993        assert!(
1994            retries
1995                .iter()
1996                .all(|attempt| attempt == &(tenant, slice, vec![1, 2])),
1997            "every retry is the identical failed slice under one key: {retries:?}"
1998        );
1999        assert_ne!(last.1, slice, "the later write has its own key");
2000        assert_eq!(state.written.get(&tenant), Some(&vec![1, 2, 3]));
2001    }
2002
2003    /// A tenant whose writes keep failing does not delay another tenant.
2004    #[tokio::test]
2005    async fn a_failing_tenant_does_not_delay_another_tenant() {
2006        let (outbox, state) = outbox(1);
2007        let failing = DataTenantId::new_v7();
2008        let healthy = DataTenantId::new_v7();
2009        state.lock().expect("unpoisoned").failing.insert(failing);
2010        outbox.stage(failing, 1_u32);
2011        await_failures(&state, 1).await;
2012        outbox.stage(healthy, 2_u32);
2013
2014        let deadline = within(10);
2015        while !state
2016            .lock()
2017            .expect("unpoisoned")
2018            .written
2019            .contains_key(&healthy)
2020        {
2021            assert!(Instant::now() < deadline, "the healthy tenant was delayed");
2022            tokio::time::sleep(Duration::from_millis(5)).await;
2023        }
2024        assert_eq!(outbox.pending(), 1, "only the failing tenant's item waits");
2025        state.lock().expect("unpoisoned").failing.clear();
2026        assert_eq!(outbox.settle(within(10)).await, 0);
2027    }
2028
2029    /// The bound covers in-flight and retrying work, not just the queue: with
2030    /// one tenant's write hanging and another's retrying, a reservation past
2031    /// the reserved share is refused, retries do not charge their slice
2032    /// again, and capacity returns once the work is written.
2033    ///
2034    /// # Panics
2035    /// Panics when a reservation within the share is refused, one past it is
2036    /// admitted, the refusal reports other counts, or the work is not written
2037    /// once in order.
2038    #[tokio::test]
2039    async fn bounded_staging_covers_retrying_and_inflight_work() {
2040        let sink = MemorySink::default();
2041        let (hang, started) = (Arc::clone(&sink.hang), Arc::clone(&sink.started));
2042        let state = Arc::clone(&sink.state);
2043        // Reservations may hold three of these four items.
2044        let outbox = Outbox::new(sink, 4, items(4));
2045        let (hanging, failing) = (DataTenantId::new_v7(), DataTenantId::new_v7());
2046        {
2047            let mut state = state.lock().expect("unpoisoned");
2048            state.hanging.insert(hanging);
2049            state.failing.insert(failing);
2050        }
2051        let reserved = outbox.reserve(1).expect("room for one reservation");
2052        reserved.stage(hanging, 1_u32);
2053        started.notified().await;
2054        let retried = outbox.reserve(2).expect("room for a two-item reservation");
2055        retried.stage(failing, 2_u32);
2056        retried.stage(failing, 3_u32);
2057        await_failures(&state, 3).await;
2058        assert_eq!(outbox.pending(), 3, "a retried slice is admitted once");
2059
2060        let refused = outbox
2061            .reserve(1)
2062            .expect_err("in-flight and retrying work fill the share");
2063        assert_eq!(
2064            (refused.admitted, refused.limit, refused.unit),
2065            (3, 3, "items")
2066        );
2067        outbox.stage(failing, 4_u32);
2068        outbox.stage(failing, 5_u32);
2069        assert_eq!(
2070            outbox.pending(),
2071            4,
2072            "unreserved staging past capacity is lost"
2073        );
2074
2075        {
2076            let mut state = state.lock().expect("unpoisoned");
2077            state.hanging.clear();
2078            state.failing.clear();
2079        }
2080        hang.notify_one();
2081        assert_eq!(
2082            outbox.settle(within(10)).await,
2083            0,
2084            "the bounded work drains"
2085        );
2086        drop((reserved, retried));
2087        let recovered = outbox
2088            .reserve(3)
2089            .expect("written work releases its capacity");
2090        recovered.stage(hanging, 6_u32);
2091        assert_eq!(outbox.settle(within(10)).await, 0);
2092        let state = state.lock().expect("unpoisoned");
2093        assert_eq!(state.written[&hanging], vec![1, 6]);
2094        assert_eq!(state.written[&failing], vec![2, 3, 4]);
2095    }
2096
2097    /// Unreserved staging on a full outbox, as audit decisions use it, returns
2098    /// at once and counts the item lost; reservations cannot take the share
2099    /// kept for it, unreserved items never count against reservations, and a
2100    /// dropped reservation returns its unspent slots.
2101    ///
2102    /// # Panics
2103    /// Panics when a full outbox blocks or keeps an unreserved item, the lost
2104    /// count differs, or reservations are admitted or refused against the
2105    /// wrong share.
2106    #[tokio::test(flavor = "current_thread")]
2107    async fn bounded_audit_staging_remains_nonblocking() {
2108        let metrics = TestMetrics::default();
2109        let _metrics = metrics::set_default_local_recorder(&metrics);
2110        let sink = MemorySink::default();
2111        let state = Arc::clone(&sink.state);
2112        let outbox = Outbox::new(sink, 4, items(4));
2113        let tenant = DataTenantId::new_v7();
2114        state.lock().expect("unpoisoned").hanging.insert(tenant);
2115
2116        let unspent = outbox.reserve(3).expect("the reserved share");
2117        outbox
2118            .reserve(1)
2119            .expect_err("reservations hold their whole share");
2120        outbox.stage(tenant, 1_u32);
2121        assert_eq!(
2122            outbox.pending(),
2123            1,
2124            "the kept share admits unreserved staging"
2125        );
2126        outbox.stage(tenant, 2_u32);
2127        assert_eq!(
2128            outbox.pending(),
2129            1,
2130            "a full unreserved share refuses without waiting"
2131        );
2132        assert_eq!(
2133            metrics.counter("outbox_events_lost_total{reason=shed}"),
2134            1,
2135            "the refusal is counted as shed"
2136        );
2137
2138        drop(unspent);
2139        outbox
2140            .reserve(3)
2141            .expect("a dropped reservation returns its slots despite the hanging item");
2142    }
2143
2144    /// A full outbox admits waiting reservations in arrival order as capacity
2145    /// is released, and refuses one whose deadline has passed.
2146    #[tokio::test(flavor = "current_thread")]
2147    async fn waiting_reservations_are_admitted_in_arrival_order() {
2148        let outbox = Outbox::new(MemorySink::default(), 4, items(4));
2149        let mut held = (0..3)
2150            .map(|_| outbox.reserve(1).expect("the reserved share"))
2151            .collect::<Vec<_>>();
2152        outbox
2153            .reserve_until(1, Instant::now())
2154            .await
2155            .expect_err("a passed deadline refuses at once");
2156
2157        let waiter = |outbox: &Arc<Outbox<MemorySink>>| {
2158            let outbox = Arc::clone(outbox);
2159            tokio::spawn(async move { outbox.reserve_until(1, within(10)).await })
2160        };
2161        let first = waiter(&outbox);
2162        tokio::task::yield_now().await;
2163        let second = waiter(&outbox);
2164        tokio::task::yield_now().await;
2165
2166        drop(held.pop());
2167        let first = first
2168            .await
2169            .expect("joined")
2170            .expect("the first waiter takes the freed slot");
2171        tokio::task::yield_now().await;
2172        assert!(!second.is_finished(), "the second waiter queues behind it");
2173
2174        drop(held.pop());
2175        let _second = second
2176            .await
2177            .expect("joined")
2178            .expect("the next freed slot admits the second waiter");
2179        drop(first);
2180    }
2181
2182    /// Capacity freed for the head of the queue is never taken by a later
2183    /// caller, even one asking for less, and a waiter that times out leaves
2184    /// the queue so the next one is admitted.
2185    #[tokio::test(flavor = "current_thread")]
2186    async fn a_later_caller_never_takes_capacity_freed_for_the_head() {
2187        let outbox = Outbox::new(MemorySink::default(), 4, items(4));
2188        let mut held = (0..3)
2189            .map(|_| outbox.reserve(1).expect("the reserved share"))
2190            .collect::<Vec<_>>();
2191        let head = {
2192            let outbox = Arc::clone(&outbox);
2193            tokio::spawn(async move { outbox.reserve_until(2, within(10)).await })
2194        };
2195        tokio::task::yield_now().await;
2196
2197        drop(held.pop());
2198        tokio::task::yield_now().await;
2199        outbox
2200            .reserve_until(1, Instant::now() + Duration::from_millis(50))
2201            .await
2202            .expect_err("a later caller does not take the head's freed slot");
2203        assert!(!head.is_finished(), "the head still needs a second slot");
2204
2205        let later = {
2206            let outbox = Arc::clone(&outbox);
2207            tokio::spawn(async move { outbox.reserve_until(1, within(10)).await })
2208        };
2209        tokio::task::yield_now().await;
2210        drop(held.pop());
2211        let head = head
2212            .await
2213            .expect("joined")
2214            .expect("the head is admitted once both slots are free");
2215        tokio::task::yield_now().await;
2216        assert!(!later.is_finished(), "the later caller still queues");
2217        drop(head);
2218        later
2219            .await
2220            .expect("joined")
2221            .expect("the head's departure admits the next caller");
2222    }
2223
2224    /// Shutdown keeps retrying until the deadline and writes what recovers.
2225    #[tokio::test]
2226    async fn shutdown_flushes_items_that_recover_before_the_deadline() {
2227        let (outbox, state) = outbox(4);
2228        let tenant = DataTenantId::new_v7();
2229        state.lock().expect("unpoisoned").failing.insert(tenant);
2230        outbox.stage(tenant, 1_u32);
2231        await_failures(&state, 1).await;
2232        let shutdown = {
2233            let outbox = Arc::clone(&outbox);
2234            tokio::spawn(async move { outbox.shutdown(within(10)).await })
2235        };
2236        await_failures(&state, 2).await;
2237        state.lock().expect("unpoisoned").failing.clear();
2238
2239        assert_eq!(shutdown.await.expect("shutdown joins"), 0, "nothing lost");
2240        assert_eq!(
2241            state.lock().expect("unpoisoned").written.get(&tenant),
2242            Some(&vec![1])
2243        );
2244        outbox.stage(tenant, 2_u32);
2245        assert_eq!(outbox.pending(), 0, "a shut-down outbox accepts nothing");
2246    }
2247
2248    /// Shutdown gives up at its deadline, reports failing and hanging items as
2249    /// lost exactly once, and leaves nothing pending in the count or gauge.
2250    #[tokio::test(flavor = "current_thread")]
2251    async fn shutdown_counts_items_unwritten_at_the_deadline_as_lost() {
2252        let metrics = TestMetrics::default();
2253        let _metrics = metrics::set_default_local_recorder(&metrics);
2254        let (outbox, state) = outbox(4);
2255        let failing = DataTenantId::new_v7();
2256        let hanging = DataTenantId::new_v7();
2257        {
2258            let mut state = state.lock().expect("unpoisoned");
2259            state.failing.insert(failing);
2260            state.hanging.insert(hanging);
2261        }
2262        outbox.stage(failing, 1_u32);
2263        outbox.stage(failing, 2_u32);
2264        outbox.stage(hanging, 3_u32);
2265        await_failures(&state, 1).await;
2266
2267        let lost = outbox
2268            .shutdown(Instant::now() + Duration::from_millis(200))
2269            .await;
2270        assert_eq!(lost, 3, "every unwritten item is reported lost");
2271        assert!(state.lock().expect("unpoisoned").written.is_empty());
2272        assert_eq!(
2273            metrics.counter("outbox_events_lost_total{reason=shutdown}"),
2274            3,
2275            "lost once, at shutdown"
2276        );
2277        assert_eq!(outbox.pending(), 0, "abandoned items are not pending");
2278        assert!(
2279            metrics.gauge("outbox_pending").abs() < f64::EPSILON,
2280            "the pending gauge returns to zero"
2281        );
2282    }
2283
2284    /// Concurrent shutdowns over reserved and unreserved items unwritten at
2285    /// the deadline report the loss once, settle both shares exactly, and
2286    /// leave an unspent reservation holding its slot until it drops.
2287    ///
2288    /// # Panics
2289    /// Panics when a reservation is refused, the loss is reported other than
2290    /// once, or a share or count does not settle.
2291    #[tokio::test(flavor = "current_thread")]
2292    async fn concurrent_shutdown_releases_reserved_losses_once() {
2293        let metrics = TestMetrics::default();
2294        let _metrics = metrics::set_default_local_recorder(&metrics);
2295        let (outbox, state) = outbox(4);
2296        let hanging = DataTenantId::new_v7();
2297        state.lock().expect("unpoisoned").hanging.insert(hanging);
2298        let held = outbox.reserve(1).expect("an unspent slot is admitted");
2299        let spent = outbox.reserve(2).expect("two reserved slots are admitted");
2300        spent.stage(hanging, 1_u32);
2301        spent.stage(hanging, 2_u32);
2302        drop(spent);
2303        outbox.stage(hanging, 3_u32);
2304
2305        let deadline = Instant::now() + Duration::from_millis(200);
2306        let (first, second) = tokio::join!(outbox.shutdown(deadline), outbox.shutdown(deadline));
2307        assert_eq!(first + second, 3, "the loss is reported by one caller");
2308        assert_eq!(first.min(second), 0, "the other caller reports zero");
2309        assert_eq!(
2310            metrics.counter("outbox_events_lost_total{reason=shutdown}"),
2311            3,
2312            "lost once, at shutdown"
2313        );
2314        assert_eq!(outbox.pending(), 0, "abandoned items are not pending");
2315        assert_eq!(outbox.admission.staged_reserved.load(Ordering::Acquire), 0);
2316        assert_eq!(outbox.admission.unreserved.load(Ordering::Acquire), 0);
2317        assert_eq!(
2318            outbox.admission.reserved.load(Ordering::Acquire),
2319            1,
2320            "only the unspent reservation still holds a slot"
2321        );
2322        drop(held);
2323        assert_eq!(outbox.admission.reserved.load(Ordering::Acquire), 0);
2324    }
2325
2326    /// A sink panic is retried like a failed write: the batch stays pending,
2327    /// commits once ahead of a later item, the writer keeps running, nothing is
2328    /// counted lost, and `pending` returns to zero.
2329    #[tokio::test(flavor = "current_thread")]
2330    async fn a_panicking_write_is_retried_once_in_order_without_loss() {
2331        let metrics = TestMetrics::default();
2332        let _metrics = metrics::set_default_local_recorder(&metrics);
2333        let (outbox, state, started) = outbox_over(MemorySink::default());
2334        let tenant = DataTenantId::new_v7();
2335        state.lock().expect("unpoisoned").panicking.insert(tenant);
2336        outbox.stage(tenant, 1_u32);
2337        started.notified().await;
2338        outbox.stage(tenant, 2_u32);
2339        assert_eq!(outbox.pending(), 2, "the panicked batch stays pending");
2340
2341        assert_eq!(outbox.settle(within(10)).await, 0, "the retry drains");
2342        assert_eq!(
2343            state.lock().expect("unpoisoned").written.get(&tenant),
2344            Some(&vec![1, 2]),
2345            "the panicked batch commits once, ahead of the later item"
2346        );
2347        assert_eq!(metrics.counter("outbox_write_failures_total"), 1);
2348        assert_eq!(metrics.counter("outbox_events_lost_total"), 0);
2349        assert!(metrics.gauge("outbox_pending").abs() < f64::EPSILON);
2350    }
2351
2352    /// A sink error whose `Display` panics is retried like a failed write: the
2353    /// batch stays pending, commits once ahead of a later item, the writer
2354    /// keeps running, the failure is counted once, nothing is counted lost,
2355    /// and `pending` and its gauge return to zero.
2356    #[tokio::test(flavor = "current_thread")]
2357    async fn a_panicking_error_display_is_retried_once_in_order_without_loss() {
2358        let metrics = TestMetrics::default();
2359        let _metrics = metrics::set_default_local_recorder(&metrics);
2360        let (outbox, state, started) = outbox_over(MemorySink::default());
2361        let tenant = DataTenantId::new_v7();
2362        state
2363            .lock()
2364            .expect("unpoisoned")
2365            .display_panicking
2366            .insert(tenant);
2367        outbox.stage(tenant, 1_u32);
2368        started.notified().await;
2369        outbox.stage(tenant, 2_u32);
2370        assert_eq!(outbox.pending(), 2, "the failed batch stays pending");
2371
2372        assert_eq!(outbox.settle(within(10)).await, 0, "the retry drains");
2373        assert_eq!(
2374            state.lock().expect("unpoisoned").written.get(&tenant),
2375            Some(&vec![1, 2]),
2376            "the failed batch commits once, ahead of the later item"
2377        );
2378        assert_eq!(metrics.counter("outbox_write_failures_total"), 1);
2379        assert_eq!(metrics.counter("outbox_events_lost_total"), 0);
2380        assert!(metrics.gauge("outbox_pending").abs() < f64::EPSILON);
2381    }
2382
2383    /// Shutdown fences admission when it begins: items staged before it drain
2384    /// even behind a blocked write, and a later stage is refused and counted
2385    /// lost without reaching the sink.
2386    #[tokio::test(flavor = "current_thread")]
2387    async fn shutdown_refuses_items_staged_after_it_begins() {
2388        let metrics = TestMetrics::default();
2389        let _metrics = metrics::set_default_local_recorder(&metrics);
2390        let sink = MemorySink::default();
2391        let hang = Arc::clone(&sink.hang);
2392        let (outbox, state, started) = outbox_over(sink);
2393        let tenant = DataTenantId::new_v7();
2394        state.lock().expect("unpoisoned").hanging.insert(tenant);
2395        outbox.stage(tenant, 1_u32);
2396        started.notified().await;
2397        outbox.stage(tenant, 2_u32);
2398
2399        let mut shutdown = pin!(outbox.shutdown(within(10)));
2400        let begun = poll_fn(|context| Poll::Ready(shutdown.as_mut().poll(context).is_pending()));
2401        assert!(begun.await, "shutdown waits for the blocked write");
2402        outbox.stage(tenant, 3_u32);
2403        assert_eq!(outbox.pending(), 2, "the late item never entered the queue");
2404        assert_eq!(
2405            metrics.counter("outbox_events_lost_total{reason=shutdown}"),
2406            1
2407        );
2408
2409        state.lock().expect("unpoisoned").hanging.clear();
2410        hang.notify_one();
2411        assert_eq!(shutdown.await, 0, "pre-fence items drain");
2412        assert_eq!(
2413            state.lock().expect("unpoisoned").written.get(&tenant),
2414            Some(&vec![1, 2]),
2415            "only pre-fence items reach the sink"
2416        );
2417        assert_eq!(outbox.pending(), 0);
2418        assert!(metrics.gauge("outbox_pending").abs() < f64::EPSILON);
2419    }
2420
2421    /// Large items shed on bytes before the item limit is reached, small ones
2422    /// are still admitted in the bytes left, written items release their
2423    /// bytes, and reservations are refused on bytes once reserved items hold
2424    /// their byte share, while a spent slot always keeps its item.
2425    ///
2426    /// # Panics
2427    /// Panics when an item is admitted past an unreserved byte limit, shed
2428    /// within it, counted under another reason, or a reservation is admitted
2429    /// or refused against the wrong byte share.
2430    #[tokio::test(flavor = "current_thread")]
2431    async fn large_items_shed_on_bytes_before_items() {
2432        let metrics = TestMetrics::default();
2433        let _metrics = metrics::set_default_local_recorder(&metrics);
2434        let sink = MemorySink::default();
2435        let (hang, state) = (Arc::clone(&sink.hang), Arc::clone(&sink.state));
2436        // Unreserved staging holds 100 bytes; reservations 300.
2437        let outbox = Outbox::new(
2438            sink,
2439            4,
2440            OutboxLimits {
2441                items: 1_024,
2442                bytes: 400,
2443            },
2444        );
2445        let tenant = DataTenantId::new_v7();
2446        state.lock().expect("unpoisoned").hanging.insert(tenant);
2447
2448        outbox.stage(tenant, 60_u32);
2449        outbox.stage(tenant, 60_u32);
2450        assert_eq!(
2451            outbox.pending(),
2452            1,
2453            "the second large item is shed on bytes"
2454        );
2455        outbox.stage(tenant, 40_u32);
2456        assert_eq!(outbox.pending(), 2, "a small item fits the bytes left");
2457        assert_eq!(metrics.counter("outbox_events_lost_total{reason=shed}"), 1);
2458
2459        let reservation = outbox.reserve(2).expect("reserved bytes are free");
2460        reservation.stage(tenant, 300_u32);
2461        let full = outbox
2462            .reserve(1)
2463            .expect_err("reserved items hold the reserved byte share");
2464        assert_eq!((full.admitted, full.limit, full.unit), (300, 300, "bytes"));
2465        reservation.stage(tenant, 50_u32);
2466        assert_eq!(
2467            outbox.pending(),
2468            4,
2469            "a spent slot keeps its item past the share"
2470        );
2471        drop(reservation);
2472
2473        state.lock().expect("unpoisoned").hanging.clear();
2474        hang.notify_one();
2475        assert_eq!(outbox.settle(within(10)).await, 0);
2476        assert_eq!(metrics.counter("outbox_items_written_total"), 4);
2477        assert_eq!(metrics.counter("outbox_bytes_written_total"), 450);
2478        assert_eq!(outbox.admission.unreserved_bytes.load(Ordering::Acquire), 0);
2479        assert_eq!(outbox.admission.reserved_bytes.load(Ordering::Acquire), 0);
2480        outbox.stage(tenant, 100_u32);
2481        outbox
2482            .reserve(1)
2483            .expect("written items release their bytes");
2484        assert_eq!(outbox.settle(within(10)).await, 0);
2485        assert_eq!(
2486            metrics.counter("outbox_events_lost_total"),
2487            1,
2488            "nothing else was lost"
2489        );
2490    }
2491
2492    /// Shed records of one tenant collected by [`ShedRecords`]: the `shed`
2493    /// count of every record, in order.
2494    #[derive(Default)]
2495    struct ShedRecords {
2496        /// The `shed` field of each record.
2497        counts: Mutex<Vec<u64>>,
2498    }
2499
2500    /// Reads the `shed` field of an event.
2501    struct ShedField<'a>(&'a mut Option<u64>);
2502
2503    impl tracing::field::Visit for ShedField<'_> {
2504        /// Keeps the value of the `shed` field.
2505        fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
2506            if field.name() == "shed" {
2507                *self.0 = Some(value);
2508            }
2509        }
2510
2511        /// Ignores every field not recorded as a `u64`.
2512        fn record_debug(&mut self, _: &tracing::field::Field, _: &dyn fmt::Debug) {}
2513    }
2514
2515    impl tracing::Subscriber for ShedRecords {
2516        /// Sees every event.
2517        fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
2518            true
2519        }
2520
2521        /// Assigns every span the same id; spans are not inspected.
2522        fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
2523            tracing::span::Id::from_u64(1)
2524        }
2525
2526        /// Ignores span records.
2527        fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
2528
2529        /// Ignores span links.
2530        fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
2531
2532        /// Keeps the `shed` count of every event carrying one.
2533        fn event(&self, event: &tracing::Event<'_>) {
2534            let mut shed = None;
2535            event.record(&mut ShedField(&mut shed));
2536            if let Some(shed) = shed {
2537                self.counts.lock().expect("unpoisoned").push(shed);
2538            }
2539        }
2540
2541        /// Ignores span entry.
2542        fn enter(&self, _: &tracing::span::Id) {}
2543
2544        /// Ignores span exit.
2545        fn exit(&self, _: &tracing::span::Id) {}
2546    }
2547
2548    /// A tenant shedding continuously writes one shed record per interval,
2549    /// and shutdown records the rest, so the records' counts sum to the
2550    /// `shed` loss counter.
2551    ///
2552    /// # Panics
2553    /// Panics when more than one record is written within the interval, a
2554    /// shed item is left unrecorded after shutdown, or the counts disagree.
2555    #[tokio::test(flavor = "current_thread")]
2556    async fn shed_records_are_aggregated_per_tenant_and_interval() {
2557        let metrics = TestMetrics::default();
2558        let _metrics = metrics::set_default_local_recorder(&metrics);
2559        let records = Arc::new(ShedRecords::default());
2560        let _records = tracing::subscriber::set_default(Arc::clone(&records));
2561        let sink = MemorySink::default();
2562        let state = Arc::clone(&sink.state);
2563        let outbox = Outbox::new(sink, 4, items(4));
2564        let (tenant, other) = (DataTenantId::new_v7(), DataTenantId::new_v7());
2565        state.lock().expect("unpoisoned").hanging.insert(tenant);
2566        outbox.stage(tenant, 1_u32);
2567        for _ in 0..5 {
2568            outbox.stage(tenant, 2_u32);
2569        }
2570        outbox.stage(other, 3_u32);
2571        assert_eq!(
2572            *records.counts.lock().expect("unpoisoned"),
2573            vec![1, 1],
2574            "one record per tenant within the interval"
2575        );
2576
2577        outbox.shutdown(Instant::now()).await;
2578        let counts = records.counts.lock().expect("unpoisoned").clone();
2579        assert_eq!(counts, vec![1, 1, 4], "shutdown records the rest");
2580        assert_eq!(
2581            counts.iter().sum::<u64>(),
2582            metrics.counter("outbox_events_lost_total{reason=shed}")
2583        );
2584    }
2585}