moqtap_proxy/egress.rs
1//! The per-stream deferred-write deque — the engine behind `Delay` and
2//! `Hold`.
3//!
4//! A [`PendingQueue`] is a **local variable** in the pipe function that already
5//! owns both halves of the stream. No task, no channel, no `Semaphore`: the
6//! reader keeps `&mut send`, so a mirrored peer reset still leaves the
7//! destination *after* the bytes already written, which is what
8//! `proxy_reset.rs::upstream_reset_reaches_the_client_as_a_reset_with_the_same_code`
9//! (*data, **then** reset*) asserts. A writer-task design reverses that
10//! ordering and cannot be made to pass it.
11//!
12//! The queue is empty and non-allocating unless a timing action is used —
13//! `VecDeque::new` does not allocate — so a session that only passes,
14//! replaces or drops never touches this module's hot path and never links
15//! [`crate::release_timer`] into any executed code.
16//!
17//! # The shape the pipe loop takes
18//!
19//! ```ignore
20//! let can_read = pending.accepts_more(); // byte + unit budget, a bool
21//! let head_release = pending.head_release(); // Option<Release>, owned clone
22//!
23//! tokio::select! {
24//! // `can_read` chooses *inside* observe_source: true polls
25//! // `recv.read`, false polls `recv.received_reset()`, which consumes
26//! // no bytes. The source is never unobserved — see `observe_source`.
27//! source = observe_source(&mut recv, &mut buf, can_read, watch) => { /* enqueue-or-write-now */ }
28//! _ = egress::wait_release(head_release.clone(), &ctx.cancel), if head_release.is_some() => {
29//! let now = Instant::now();
30//! while let Some(unit) = pending.pop_next_due(now) {
31//! pending.record_release(&unit, now);
32//! egress::write_unit(unit, &mut send).await?;
33//! }
34//! }
35//! _ = ctx.cancel.cancelled() => { /* framer.finish() flush, then drain, then return */ }
36//! }
37//! ```
38//!
39//! Both branch *expressions* are free of any borrow of `pending`: one is a
40//! `bool`, the other an owned [`Release`] (one `Arc` clone plus one token
41//! clone). That is a **rule, not a compile error** — a branch future holding
42//! `&mut pending` whose winning body then calls `pending.pop_next_due()`
43//! does in fact compile, because `select!` drops the branch-future tuple
44//! before the handler runs. Keeping the expressions borrow-free is what lets
45//! [`wait_release`] be a free function shared by the release branch and by
46//! [`drain_honouring_release_times`], and lets a later edit add a fourth
47//! branch without re-arranging the other three.
48//!
49//! **Why [`tokio_util::sync::CancellationToken`] and not a `Notify` or a
50//! waker registration**: it is level-triggered. The `select!` may create and
51//! drop a fresh `cancelled()` future on every iteration with no lost-wakeup
52//! window and no re-registration, whereas a `Notify`-based wheel would need
53//! a global-mutex round trip per read chunk to re-register.
54//! `if head_release.is_some()` matters for the same reason from the other
55//! side: tokio does not evaluate a branch's async expression when its
56//! precondition is false, so an empty queue costs one `Option::is_some()`.
57//!
58//! # Cancellation
59//!
60//! Every wait in this module is a `select!` **branch**, never an arm body:
61//! an arm body is not preemptible, so an inline sleep there starves the
62//! cancel branch and a `Hold` on a gate nobody releases would pin session
63//! teardown for up to [`EgressConfig::max_hold`] (30 s by default). That
64//! applies to the two release-honouring drains as much as to the pipe loop,
65//! which is why [`drain_honouring_release_times`] is written as a `biased`
66//! race against cancellation with
67//! [`PendingQueue::drain_ignoring_release_times`] as its fallback.
68//!
69//! # Ordering is the queue's; readiness is the unit's
70//!
71//! The two are **separate properties**, and conflating them is what the
72//! first shape of this module got wrong.
73//!
74//! * *Ordering* belongs to the deque. [`PendingQueue::pop_next_due`] only
75//! ever looks at the **front**, so nothing can be written before
76//! everything ahead of it has been. That invariant needs no arithmetic
77//! at all, and it is absolute: anything else silently corrupts MoQT
78//! stream semantics.
79//! * *Readiness* belongs to the unit. A [`Pending`] is due when
80//! [`Pending::due_at`] has passed **or** its own `Hold` gate is open —
81//! see [`Pending::is_due`]. Nothing that happens to a unit ahead of it
82//! may rewrite that.
83//!
84//! So [`PendingQueue::push`] does **not** clamp `due_at` against the tail.
85//! An earlier shape did, and it turned a `Hold`'s
86//! [`EgressConfig::max_hold`] ceiling into a *deadline conferred on every
87//! successor*: releasing the gate woke only the unit carrying it, and
88//! objects queued behind — including the stream's FIN, which waits for the
89//! queue to drain — sat until `max_hold` (30 s by default). Worse, a
90//! `Delay`'s own deadline was overwritten by the ceiling of a `Hold` in
91//! front of it, so the deadline `Action::Delay` documents ("`arrived_at +
92//! by`, a deadline, not a spacing") was not the one the engine honoured.
93//!
94//! What the clamp *was* for is still wanted, and it survives as a separate
95//! field: [`Pending::expected_at`] is `max(due_at, tail.expected_at, now)`,
96//! the queue's estimate of when the unit will actually reach the wire given
97//! everything ahead of it. It is what [`Push::release_at`] reports as
98//! `Effect::Queued { release_at }` and what [`PendingQueue::record_release`]
99//! measures lateness against, so a unit queued behind a 300 ms delay is not
100//! logged as a 300 ms timer error. It never gates anything.
101//!
102//! # Bound
103//!
104//! [`PendingQueue::accepts_more`] is `false` once the queue holds
105//! [`EgressConfig::max_pending_bytes`] (1 MiB default) **or**
106//! [`MAX_PENDING_UNITS`] units. The pipe loop stops calling `recv.read` on
107//! `false`, so the queue cannot grow past that plus one in-flight read
108//! chunk. **It does not stop observing the source**: it swaps the read for
109//! `RecvStream::received_reset`, which consumes nothing, so the bound here
110//! is unchanged and a peer's `RESET_STREAM` is still seen at once. The
111//! unit cap is not redundant with the byte cap: an elided unit
112//! holds an ordering slot and carries zero bytes, so a hook that elides
113//! every object behind a never-released `Hold` would otherwise grow the
114//! deque without bound at zero bytes. Crossing either cap is reported once
115//! per stream — see [`Push::entered_backpressure`].
116
117use std::collections::VecDeque;
118use std::future::Future;
119use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
120use std::sync::{Arc, OnceLock};
121use std::time::{Duration, Instant};
122
123use bytes::Bytes;
124use tokio::sync::Notify;
125use tokio_util::sync::CancellationToken;
126
127use moqtap_client::transport::{SendStream, TransportError};
128
129use crate::action::{EgressConfig, Gate};
130use crate::error::ProxyError;
131use crate::instrument::Recorder;
132use crate::release_timer::{self, Deadline};
133use crate::shape::{Acquire, Class, Expiry, QueueDepth, Scheduler, ShapeRecorder};
134use crate::types::ProxySide;
135
136/// Hard cap on queued units, independent of the byte budget.
137///
138/// A [`Pending`] is on the order of 64 bytes, so this is roughly the same
139/// order of bookkeeping as the default 1 MiB byte budget, and it is far
140/// more objects than any realistic hold window. It exists because
141/// [`Item::Elided`] units carry zero bytes: without it, eliding every
142/// object behind a never-released `Hold` grows the deque without bound
143/// while `queued_bytes()` stays at zero.
144pub(crate) const MAX_PENDING_UNITS: usize = 8192;
145
146/// What writing to a destination can fail with.
147///
148/// Deliberately narrower than [`ProxyError`]: this module hands bytes to a
149/// transport and abandons streams, and those are the only two ways it can
150/// fail. Naming them separately is what lets a sink be implemented without
151/// naming a crate-wide error enum whose listener, TLS, certificate, qlog and
152/// draft-admission cases a write can never produce — and the recording sink
153/// below is the second implementor, so "without naming it" is not
154/// hypothetical.
155///
156/// Nothing a caller sees changes: the conversion below is applied by `?` at
157/// every boundary this module is called across, and it produces the same
158/// [`ProxyError`] the failure produced before.
159///
160/// One case, and the enum is still the right shape. Every failure reaching
161/// here today comes back from the transport, which is a fact about the two
162/// operations rather than about the one production implementor — so a second
163/// case would be a second *kind of failure*, not a second sink, and would want
164/// a name of its own. The recording sink below models a refused write as a
165/// refused write for the same reason: a test sink inventing a failure the
166/// transport cannot produce would be testing a path nothing takes.
167#[derive(Debug, thiserror::Error)]
168pub(crate) enum EgressError {
169 /// The transport refused a write or a reset.
170 #[error("transport error: {0}")]
171 Transport(#[from] TransportError),
172}
173
174impl From<EgressError> for ProxyError {
175 fn from(error: EgressError) -> Self {
176 let EgressError::Transport(source) = error;
177 ProxyError::Transport(source)
178 }
179}
180
181// ── The destination ─────────────────────────────────────────────────
182
183/// The write half this module drives.
184///
185/// A trait rather than a bare `&mut SendStream` for exactly one reason:
186/// the ordering, clamping, terminal and cancellation behaviour below is
187/// then testable in-process against a recording sink, with no QUIC
188/// handshake and no timing. The only production implementor is
189/// [`SendStream`].
190///
191/// `write_all` returns `impl Future + Send` rather than being an
192/// `async fn`: the forwarding tasks are spawned onto a multi-threaded
193/// runtime, so every future this module builds has to be `Send`, and an
194/// `async fn` in a trait carries no such bound.
195pub(crate) trait EgressSink {
196 /// Hand `buf` to the transport.
197 fn write_all(&mut self, buf: &[u8]) -> impl Future<Output = Result<(), EgressError>> + Send;
198 /// Abandon the stream with `code`, discarding anything still in the
199 /// local send buffer.
200 fn reset(&mut self, code: u64) -> Result<(), EgressError>;
201}
202
203impl EgressSink for SendStream {
204 // `async fn` here still satisfies the trait's `impl Future + Send`:
205 // the compiler checks the Send-ness of the generated future against
206 // the bound, which is the whole reason the bound is on the trait.
207 // Inherent methods win method resolution, so the call below is the
208 // transport's own `write_all`, not this trait's.
209 async fn write_all(&mut self, buf: &[u8]) -> Result<(), EgressError> {
210 SendStream::write_all(self, buf).await.map_err(EgressError::from)
211 }
212
213 fn reset(&mut self, code: u64) -> Result<(), EgressError> {
214 SendStream::reset(self, code).map_err(EgressError::from)
215 }
216}
217
218// ── Release handles ─────────────────────────────────────────────────
219
220/// What the head of a queue is waiting for. Owned, cheap to clone.
221///
222/// Cloning is one `Arc` clone plus one or two token clones. Nothing here
223/// borrows the queue, which is what keeps [`wait_release`] a free function
224/// usable from both the pipe loop's release branch and the drains.
225#[derive(Clone, Debug)]
226pub(crate) struct Release {
227 /// The wheel's handle for `release_at`. Registration happened once, in
228 /// [`PendingQueue::arm_head`]; awaiting this registers nothing.
229 deadline: Deadline,
230 /// A `Hold`'s gate, when the head carries one.
231 gate: Option<Gate>,
232}
233
234// Read only by this module's own tests today — `session.rs` awaits a
235// `Release` through `wait_release` and never inspects it, and `exec.rs`
236// never sees one. Kept because it is part of this type's deliberate read
237// surface and because the fast-path test asserts on the deadline's token
238// directly; the allow is on the one item rather than on the file, so
239// anything that becomes dead later is still a build failure.
240#[allow(dead_code)]
241impl Release {
242 /// The wheel handle, for callers that want to observe it directly.
243 pub(crate) fn deadline(&self) -> &Deadline {
244 &self.deadline
245 }
246}
247
248/// Resolve when the head unit may be written, or when the session is torn
249/// down — whichever comes first.
250///
251/// Races three level-triggered tokens: the wheel's deadline, the unit's
252/// `Hold` gate (a released gate makes a unit due early — see
253/// [`Pending::is_due`]) and session cancellation. It **registers nothing**:
254/// every one of the three is a `CancellationToken`, so creating and
255/// dropping the futures on each `select!` iteration is free of lost-wakeup
256/// races.
257///
258/// `None` parks until cancellation. Callers guard the branch with
259/// `if head_release.is_some()`, so that path is a safety net rather than a
260/// normal outcome: it must not resolve immediately, or a caller that forgot
261/// the guard would spin the loop hot on an empty queue.
262pub(crate) async fn wait_release(release: Option<Release>, cancel: &CancellationToken) {
263 let Some(release) = release else {
264 cancel.cancelled().await;
265 return;
266 };
267 match release.gate {
268 Some(gate) => {
269 tokio::select! {
270 () = release.deadline.token().cancelled() => {}
271 () = gate.wait() => {}
272 () = cancel.cancelled() => {}
273 }
274 }
275 None => {
276 tokio::select! {
277 () = release.deadline.token().cancelled() => {}
278 () = cancel.cancelled() => {}
279 }
280 }
281 }
282}
283
284// ── Queued units ────────────────────────────────────────────────────
285
286/// A positional stream ending.
287///
288/// Enqueued *behind* the current queue contents, so everything decided
289/// before it is written first. Nothing may be queued behind one.
290#[derive(Clone, Debug, PartialEq, Eq)]
291pub(crate) enum Terminal {
292 /// Write `prefix`, then `RESET_STREAM` with `code` —
293 /// [`crate::action::Action::Truncate`].
294 ///
295 /// The peer observes **at most** `prefix.len()` further bytes and may
296 /// observe none: quinn clears the receive assembler when the reset is
297 /// processed, and `reset()` discards the local send buffer too.
298 Truncate {
299 /// The prefix of the unit to write before resetting.
300 prefix: Bytes,
301 /// The application error code for the reset.
302 code: u64,
303 },
304 /// `RESET_STREAM` with `code` and nothing further —
305 /// [`crate::action::Action::ResetStream`].
306 Reset {
307 /// The application error code for the reset.
308 code: u64,
309 },
310}
311
312impl Terminal {
313 /// Bytes this terminal still owes the wire.
314 fn len(&self) -> usize {
315 match self {
316 Terminal::Truncate { prefix, .. } => prefix.len(),
317 Terminal::Reset { .. } => 0,
318 }
319 }
320}
321
322/// What a queued unit does when it is written.
323#[derive(Clone, Debug, PartialEq, Eq)]
324pub(crate) enum Item {
325 /// Write these bytes. Already whatever the action made of them —
326 /// verbatim for `Pass`, spliced for `ReplacePayload`, substituted for
327 /// `Replace`.
328 Write(Bytes),
329 /// Write nothing.
330 ///
331 /// An elided unit still takes a slot when the queue is non-empty: it
332 /// was decided at a point in the stream, and dropping it out of band
333 /// would let a later unit be written before an earlier one is.
334 Elided,
335 /// End the stream. See [`Terminal`].
336 Terminal(Terminal),
337}
338
339/// What the shaper attached to one queued unit, on a shaped stream.
340///
341/// `None` on every unit of every unshaped stream, which is what makes "a
342/// session with no `ShapeProfile` adds nothing to this path" a fact the type
343/// carries rather than a claim a reviewer checks.
344#[derive(Clone, Copy, Debug)]
345struct ShapeTag {
346 /// The row this unit's bytes are charged to, and the bucket it draws
347 /// from. Resolved per unit at admission — never per stream, because
348 /// `object_id` and `every_nth` are legal class selectors and a
349 /// stream-sticky class would evaluate them on object 0 alone.
350 class: Class,
351 /// The instant this unit goes out **anyway**: `arrived_at` plus the
352 /// profile's `max_hold`, or the engine's when the profile inherits it.
353 ///
354 /// Under the default `Expiry::Deliver` that is a clamp, not a drop — so
355 /// a starved class is late, never silently lossy, and "delivers zero
356 /// bytes" is always a statement about a sampling window.
357 expires_at: Instant,
358 /// Whether this unit has already been counted against
359 /// `starved_behind_other_class`. Once per unit, not once per wake.
360 starved_noted: bool,
361}
362
363/// One unit of traffic waiting for its release time.
364#[derive(Clone, Debug)]
365pub(crate) struct Pending {
366 /// The earliest instant **this** unit may be written, and the only instant
367 /// that gates it. Set once, by whoever built the unit, and never rewritten
368 /// by the queue — see the module doc's *Ordering is the queue's; readiness
369 /// is the unit's*.
370 due_at: Instant,
371 /// When the queue *expects* to write it, given everything ahead of it:
372 /// `max(due_at, tail.expected_at, now)` at push time.
373 ///
374 /// Reporting and lateness only. It gates nothing, and it is
375 /// deliberately an over-estimate for a unit queued behind a `Hold`,
376 /// whose gate may open long before its [`EgressConfig::max_hold`]
377 /// ceiling.
378 expected_at: Instant,
379 /// A `Hold`'s gate. Releasing it makes the unit due before `due_at`,
380 /// which for a held unit is the [`EgressConfig::max_hold`] ceiling.
381 gate: Option<Gate>,
382 /// What writing it does.
383 item: Item,
384 /// The shaper's tag, on a shaped stream only. Attached by
385 /// [`PendingQueue::push`] from the class the pipe loop most recently
386 /// resolved, so `exec`'s pushes carry it without `exec` naming a class.
387 shape: Option<ShapeTag>,
388}
389
390impl Pending {
391 /// Bytes to write no earlier than `due_at`.
392 pub(crate) fn bytes(raw: Bytes, due_at: Instant) -> Self {
393 Self { due_at, expected_at: due_at, gate: None, item: Item::Write(raw), shape: None }
394 }
395
396 /// An ordering placeholder that writes nothing.
397 pub(crate) fn elided(due_at: Instant) -> Self {
398 Self { due_at, expected_at: due_at, gate: None, item: Item::Elided, shape: None }
399 }
400
401 /// A positional terminal, due as soon as the queue ahead of it drains.
402 pub(crate) fn terminal(terminal: Terminal) -> Self {
403 let now = Instant::now();
404 Self {
405 due_at: now,
406 expected_at: now,
407 gate: None,
408 item: Item::Terminal(terminal),
409 shape: None,
410 }
411 }
412
413 /// Attach a `Hold`'s gate. `due_at` stays the `max_hold` ceiling.
414 #[must_use]
415 pub(crate) fn with_gate(mut self, gate: Gate) -> Self {
416 self.gate = Some(gate);
417 self
418 }
419
420 // The observers below are read by this module's tests and by nothing
421 // else: the queue answers `due_at` through `head_release()`, and a
422 // terminal announces itself through `Written::Terminated` rather than
423 // being asked. They stay as the deliberate read surface of a unit,
424 // with the allow scoped to them rather than to the file.
425 /// When this unit becomes due on its own account.
426 #[allow(dead_code)]
427 pub(crate) fn due_at(&self) -> Instant {
428 self.due_at
429 }
430
431 /// When the queue expected to write it. Never a gate; see the field.
432 #[allow(dead_code)]
433 pub(crate) fn expected_at(&self) -> Instant {
434 self.expected_at
435 }
436
437 /// What it does. Terminals end the stream.
438 #[allow(dead_code)]
439 pub(crate) fn item(&self) -> &Item {
440 &self.item
441 }
442
443 /// Whether this unit ends the stream.
444 #[allow(dead_code)]
445 pub(crate) fn is_terminal(&self) -> bool {
446 matches!(self.item, Item::Terminal(_))
447 }
448
449 /// Bytes this unit is holding on the queue's behalf.
450 pub(crate) fn len(&self) -> usize {
451 match &self.item {
452 Item::Write(raw) => raw.len(),
453 Item::Elided => 0,
454 Item::Terminal(t) => t.len(),
455 }
456 }
457
458 /// Whether this unit may be written at `now`, **on its own account**.
459 ///
460 /// Readiness only: whether anything is still queued ahead of it is the
461 /// deque's business, and [`PendingQueue::pop_next_due`] answers that by
462 /// looking at the front and nowhere else.
463 ///
464 /// A **released gate makes it due**, regardless of `due_at`. Without
465 /// that clause, releasing a `Hold` early wakes the release branch to
466 /// find nothing due and the loop spins hot until
467 /// [`EgressConfig::max_hold`] — one of the two ways this loop can spin
468 /// hot, and the one that is invisible from the call site.
469 ///
470 /// `expected_at` is deliberately **not** consulted: it is the queue's
471 /// estimate, and for a unit behind a `Hold` it is that hold's ceiling.
472 /// Gating on it is what stranded whole streams behind a released gate.
473 fn is_due(&self, now: Instant) -> bool {
474 self.due_at <= now || self.gate.as_ref().is_some_and(Gate::is_released)
475 }
476}
477
478/// What handing one unit to the transport did.
479#[derive(Clone, Copy, Debug, PartialEq, Eq)]
480pub(crate) enum Written {
481 /// This many bytes were handed to the transport.
482 Bytes(usize),
483 /// Nothing was written — an elided unit holding its ordering slot.
484 Nothing,
485 /// A terminal fired. The destination is reset; nothing further may be
486 /// written to it.
487 Terminated {
488 /// Bytes written ahead of the reset, on this unit.
489 forwarded: usize,
490 /// The code the stream was reset with.
491 code: u64,
492 },
493}
494
495/// Write one popped unit.
496///
497/// A failing prefix write on a [`Terminal::Truncate`] is propagated rather
498/// than swallowed: the caller mirrors it with `propagate_stop`, and the
499/// reset it would have sent is moot once the peer has stopped us. A failing
500/// `reset` is *not* propagated — it means the stream was already finished
501/// or reset, so there is nothing left to report and nothing left to do.
502pub(crate) async fn write_unit<S: EgressSink>(
503 unit: Pending,
504 send: &mut S,
505) -> Result<Written, EgressError> {
506 match unit.item {
507 Item::Write(raw) => {
508 send.write_all(&raw).await?;
509 Ok(Written::Bytes(raw.len()))
510 }
511 Item::Elided => Ok(Written::Nothing),
512 Item::Terminal(Terminal::Truncate { prefix, code }) => {
513 if !prefix.is_empty() {
514 send.write_all(&prefix).await?;
515 }
516 let _ = send.reset(code);
517 Ok(Written::Terminated { forwarded: prefix.len(), code })
518 }
519 Item::Terminal(Terminal::Reset { code }) => {
520 let _ = send.reset(code);
521 Ok(Written::Terminated { forwarded: 0, code })
522 }
523 }
524}
525
526// ── Delay and hold arithmetic ───────────────────────────────────────
527
528/// A resolved release deadline, and whether [`EgressConfig::max_hold`] cut
529/// it short.
530#[derive(Clone, Copy, Debug, PartialEq, Eq)]
531pub(crate) struct Deferral {
532 /// The deadline to put on the [`Pending`], before the queue's own
533 /// monotonic clamp.
534 pub(crate) release_at: Instant,
535 /// What was asked for.
536 pub(crate) requested: Duration,
537 /// What was applied.
538 pub(crate) applied: Duration,
539}
540
541impl Deferral {
542 /// Whether `max_hold` cut the request short — emit
543 /// `Impairment { HoldClamped { requested, applied } }`, once per
544 /// clamped unit.
545 pub(crate) fn was_clamped(&self) -> bool {
546 self.applied < self.requested
547 }
548}
549
550/// Resolve `Delay { by }` against [`EgressConfig::max_hold`].
551///
552/// A **deadline**, not a spacing: `arrived_at + by`. Two units arriving
553/// together with the same `by` are released together, not staggered.
554pub(crate) fn defer_by(arrived_at: Instant, by: Duration, config: &EgressConfig) -> Deferral {
555 let applied = by.min(config.max_hold);
556 Deferral { release_at: arrived_at + applied, requested: by, applied }
557}
558
559/// The ceiling on an [`crate::action::Action::Hold`]: a gate nobody
560/// releases is still written at `arrived_at + max_hold`, so a hook cannot
561/// pin a stream open forever.
562pub(crate) fn hold_ceiling(arrived_at: Instant, config: &EgressConfig) -> Instant {
563 arrived_at + config.max_hold
564}
565
566// ── The queue ───────────────────────────────────────────────────────
567
568/// What [`PendingQueue::push`] did.
569#[derive(Clone, Copy, Debug, PartialEq, Eq)]
570pub(crate) struct Push {
571 /// [`Pending::expected_at`] — the queue's estimate of when this unit
572 /// reaches the wire, which is the value to report as
573 /// `Effect::Queued { release_at }`. It accounts for everything already
574 /// queued, so it may be later than the action asked for, and for a unit
575 /// behind a `Hold` it is that hold's `max_hold` ceiling: an upper
576 /// bound, since a gate may open at any time before it.
577 ///
578 /// **Not** the unit's own deadline. That is [`Pending::due_at`], it is
579 /// the only thing that gates the unit, and `push` never rewrites it.
580 pub(crate) release_at: Instant,
581 /// `true` exactly once per stream: on the push after which
582 /// [`PendingQueue::accepts_more`] first turns `false`. Emit
583 /// `Impairment { EgressQueueFull { stream_id } }` on it, and only on
584 /// it — a queue that drains and fills again does not report twice.
585 pub(crate) entered_backpressure: bool,
586}
587
588/// How a drain ended.
589#[derive(Clone, Copy, Debug, PartialEq, Eq)]
590pub(crate) enum DrainOutcome {
591 /// Every queued unit was written.
592 Complete,
593 /// Cancellation won mid-drain. The remainder was flushed ignoring
594 /// release times; [`PendingQueue::unconfirmed_bytes`] afterwards is
595 /// what that fallback either could not write **or** wrote into a
596 /// transport that is being closed out from under it, and is what
597 /// `Impairment { QueuedBytesAtTeardown }` carries.
598 CancelledMidDrain,
599 /// A queued terminal fired. The destination is reset and nothing
600 /// further may be written to it.
601 Terminated {
602 /// Bytes written on the terminal unit itself, ahead of the reset.
603 forwarded: usize,
604 /// The code the stream was reset with.
605 code: u64,
606 },
607 /// A write failed. The failing unit and everything behind it are still
608 /// queued. Only the best-effort drain returns this; the
609 /// release-honouring drain propagates the error instead.
610 WriteFailed,
611 /// The teardown flush declined to write anything, because the session's
612 /// [`EgressGauge`] is discarding: a requested close ran out of drain
613 /// time, so what is left is reported rather than handed to a connection
614 /// that is about to be closed. The queue is untouched, so
615 /// [`PendingQueue::unconfirmed_bytes`] is exactly what was abandoned.
616 Discarded,
617}
618
619/// How many bytes one session's egress queues are holding, right now.
620///
621/// One gauge per session, shared by every [`PendingQueue`] the session
622/// builds. It exists for a single caller — a requested close, which has to
623/// know when there is nothing left to flush so it can stop waiting early
624/// — and it answers that question in the one unit that is worth waiting on.
625///
626/// # Why a byte gauge rather than a stream count
627///
628/// A session's live streams are already enumerated by
629/// [`StreamRegistry`](crate::control::StreamRegistry), and waiting for that
630/// to empty was the obvious alternative. It is the wrong signal twice over:
631/// a stream with an empty queue keeps its registration until its source
632/// FINs, so the wait would run to the deadline on a session that had
633/// nothing to flush at all; and a stream that ends while its queue is full
634/// drops its registration with the bytes still unwritten, so the wait would
635/// finish claiming a drain that lost data.
636///
637/// # Two-valued, and the second value is the whole bound
638///
639/// [`Self::wait_idle`] is the wait. [`Self::begin_discarding`] is what
640/// happens when the wait runs out: it puts every queue in the session into
641/// a mode where [`PendingQueue::drain_ignoring_release_times`] writes
642/// nothing at all.
643///
644/// That looks backwards next to the rest of this module, whose teardown rule is
645/// *delivered late beats lost silently*. The difference is that an ordinary
646/// teardown is not a deadline anybody set — the flush is the best remaining
647/// chance those bytes have. A close that has already been given its drain
648/// window and spent it is a deadline somebody set, and flushing past it makes
649/// the outcome *unreportable*: a `write_all` into quinn returns `Ok` as soon as
650/// the bytes are buffered and `Connection::close` discards that buffer, so the
651/// bytes are neither confirmably delivered nor confirmably lost, and no count
652/// of them adds up. Declining to write keeps the arithmetic exact — what the
653/// peer received plus what
654/// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown)
655/// names equals what was queued when the close was requested — which is the
656/// property a gate can actually be written against.
657#[derive(Debug)]
658pub(crate) struct EgressGauge {
659 /// Bytes queued across every live [`PendingQueue`] of this session.
660 queued: AtomicUsize,
661 /// Pulsed whenever `queued` reaches zero.
662 idle: Notify,
663 /// Whether teardown flushes are declined — see the type's own doc.
664 discarding: AtomicBool,
665}
666
667impl EgressGauge {
668 /// A gauge reading zero, flushing normally.
669 pub(crate) fn new() -> Arc<Self> {
670 Arc::new(Self {
671 queued: AtomicUsize::new(0),
672 idle: Notify::new(),
673 discarding: AtomicBool::new(false),
674 })
675 }
676
677 /// Bytes queued across the session at this instant.
678 pub(crate) fn queued(&self) -> usize {
679 self.queued.load(Ordering::Acquire)
680 }
681
682 /// Whether teardown flushes are being declined.
683 pub(crate) fn is_discarding(&self) -> bool {
684 self.discarding.load(Ordering::Acquire)
685 }
686
687 /// Decline every later teardown flush on this session.
688 ///
689 /// One-way: nothing turns it off again, because the only caller is a
690 /// close whose drain window has expired and a session in that state is
691 /// on its way down.
692 pub(crate) fn begin_discarding(&self) {
693 self.discarding.store(true, Ordering::Release);
694 }
695
696 /// Charge `bytes` to the session total.
697 fn add(&self, bytes: usize) {
698 if bytes > 0 {
699 self.queued.fetch_add(bytes, Ordering::AcqRel);
700 }
701 }
702
703 /// Credit `bytes` back, waking a waiter when the session reaches zero.
704 fn sub(&self, bytes: usize) {
705 if bytes == 0 {
706 return;
707 }
708 // `saturating` in effect: a queue never credits back more than it
709 // charged, but an underflow here would wrap to a total that never
710 // reaches zero and would turn every later close into a full-length
711 // wait, so the arithmetic is written not to be able to. The loop is
712 // spelled out because the closure form is `try_update` from Rust 1.99
713 // and `fetch_update`, deprecated there, is the only name under the
714 // 1.88 MSRV.
715 let mut before = self.queued.load(Ordering::Acquire);
716 while let Err(seen) = self.queued.compare_exchange_weak(
717 before,
718 before.saturating_sub(bytes),
719 Ordering::AcqRel,
720 Ordering::Acquire,
721 ) {
722 before = seen;
723 }
724 if before.saturating_sub(bytes) == 0 {
725 self.idle.notify_waiters();
726 }
727 }
728
729 /// Wait for the session's queues to empty, for at most `timeout`.
730 ///
731 /// Answers the bytes still queued when it returns: `0` when everything
732 /// drained, and the residue when the deadline won. That residue is the
733 /// figure a caller acts on — it is what will be abandoned — and it is
734 /// deliberately a byte count and not "did it time out", because the
735 /// two differ: a queue that empties on the last poll before the
736 /// deadline drained, and one that reports zero after the deadline
737 /// drained too.
738 ///
739 /// The registration is armed **before** the count is re-read, so a
740 /// queue that empties between the two is not a lost wakeup that costs
741 /// the caller its whole timeout.
742 pub(crate) async fn wait_idle(&self, timeout: Duration) -> usize {
743 let deadline = tokio::time::sleep(timeout);
744 tokio::pin!(deadline);
745 loop {
746 if self.queued() == 0 {
747 return 0;
748 }
749 let notified = self.idle.notified();
750 tokio::pin!(notified);
751 notified.as_mut().enable();
752 if self.queued() == 0 {
753 return 0;
754 }
755 tokio::select! {
756 () = &mut notified => {}
757 () = &mut deadline => return self.queued(),
758 }
759 }
760 }
761}
762
763/// A per-stream FIFO of units waiting for their release times.
764///
765/// Live as a local variable in `pipe_data_framed` / `pipe_control_mutating`
766/// for the lifetime of one stream direction.
767#[derive(Debug)]
768pub(crate) struct PendingQueue {
769 q: VecDeque<Pending>,
770 /// The wheel handle for `q.front()`. Invariant:
771 /// `head_deadline.is_some() == !q.is_empty()`.
772 head_deadline: Option<Deadline>,
773 queued_bytes: usize,
774 /// Bytes a teardown drain handed to the transport, which nothing can
775 /// confirm ever left it. See [`Self::unconfirmed_bytes`].
776 flushed_unconfirmed: usize,
777 /// The session-wide byte gauge this queue reports into, or `None` for a
778 /// queue built outside a session — every `#[cfg(test)]` queue in this
779 /// module, which has no session to report to.
780 ///
781 /// Mirrored rather than derived: nothing can walk a session's queues,
782 /// because each one is a local variable in the pipe function that owns
783 /// its stream. Every write to `queued_bytes` goes through
784 /// [`Self::charge`] or [`Self::credit`] so the mirror cannot drift from
785 /// the value it mirrors.
786 gauge: Option<Arc<EgressGauge>>,
787 config: EgressConfig,
788 counters: Arc<Recorder>,
789 /// Report-once latch for [`Push::entered_backpressure`].
790 backpressure_reported: bool,
791 /// The shaping profile's per-stream depth, when this stream's session
792 /// has one **and** its overflow policy is `Block`.
793 ///
794 /// `None` on every unshaped stream and under every non-blocking
795 /// overflow: `DropTail` needs the read branch to stay enabled or
796 /// nothing arrives to be dropped, and `ResetStream` is decided on the
797 /// arriving unit. See [`Self::accepts_more`].
798 shape_depth: Option<QueueDepth>,
799 /// The session's shaper, on a shaped **data** stream and nowhere else.
800 /// `None` on every unshaped stream and on both control pipes, which is what
801 /// makes *the control pipes are never shaped* structural: a control queue
802 /// has no scheduler to ask, so no control frame can reach a bucket even by
803 /// accident.
804 shaper: Option<Arc<Scheduler>>,
805 /// Where a shaped release records what it did. Carried beside the
806 /// scheduler rather than reached through it because the recorder is
807 /// always constructed and the scheduler is not.
808 shape_stats: Option<Arc<ShapeRecorder>>,
809 /// The side this stream's traffic **arrived** on, for the per-leg and
810 /// per-direction figures the recorder keeps.
811 ///
812 /// `Option`, and deliberately not a defaulted side: one recorder serves
813 /// both legs of a session, so a queue that guessed would charge real
814 /// bytes to the wrong side and the figure would still look plausible.
815 /// `Some` exactly when `shape_stats` is — the same builder sets both —
816 /// so every reader below takes them together.
817 ///
818 /// A side rather than a direction because the recorder needs the leg
819 /// too, and this queue is the only thing that knows which stream it is
820 /// serving. It stays the *arrival* side even for the release figures the
821 /// proxy charges to the leg a unit leaves by: the turn-around belongs to
822 /// the recorder, which is one place, rather than to every queue, which
823 /// is one per stream.
824 shape_side: Option<ProxySide>,
825 /// `max_hold` for this stream's shaped units: the profile's, or the
826 /// engine's when the profile inherits it.
827 shape_hold: Duration,
828 /// The class the pipe loop resolved for the unit it is currently
829 /// handling. Read — not taken — by [`Self::push`], so every push made
830 /// while one framed unit is being executed carries the same class, and
831 /// the next unit overwrites it.
832 unit_class: Class,
833 /// The gate the head is parked on because a [`Discipline`] is holding
834 /// its class back. Merged into [`Self::head_release`] so `wait_release`
835 /// races it exactly as it races a `Hold`'s gate.
836 ///
837 /// [`Discipline`]: crate::shape::Discipline
838 shape_gate: Option<Gate>,
839 /// Whether the shaper is currently holding the head back.
840 ///
841 /// It decides which gate [`Self::head_release`] reports, and that is a
842 /// **spin guard**, not bookkeeping. A `Hold` whose gate has been released
843 /// makes its unit due (`Pending::is_due`) and the gate stays released
844 /// forever; if the queue kept reporting it while the shaper was refusing
845 /// the same unit, `wait_release` would resolve on every `select!`
846 /// iteration and the pipe loop would spin hot until the bucket refilled.
847 ///
848 /// Once a unit is due, the only things that may wake its stream are the
849 /// deadline the scheduler armed and the gate a discipline handed back.
850 shape_parked: bool,
851 /// The class this queue has declared demand for with the scheduler.
852 /// Exactly one declaration is outstanding at a time, and [`Drop`] is what
853 /// guarantees it is withdrawn.
854 shape_demand: Option<Class>,
855 /// Edge latch for `tokens_exhausted_episodes` — armed when a release is
856 /// refused for want of tokens, re-armed when one is granted.
857 tokens_dry: bool,
858 /// The index the next `starved_behind_other_class` sweep starts from, so
859 /// each queued unit is examined once over the queue's whole life rather
860 /// than once per wake.
861 starved_from: usize,
862 /// What the last [`Self::pop_next_due`] wants reported, for a caller that
863 /// has an `exec::Reporter` and an event sink. Taken, never accumulated:
864 /// at most one unit is released per call.
865 shape_report: Option<ShapeReport>,
866}
867
868/// What a shaped release wants the pipe loop to report.
869///
870/// The queue owns the decision and the caller owns the reporting: `egress`
871/// has no `Reporter`, no `ProxyObserver` and no session id, and giving it
872/// one would put the whole event surface behind a type whose job is a deque.
873#[derive(Clone, Copy, Debug, PartialEq, Eq)]
874pub(crate) enum ShapeReport {
875 /// A unit went out at its `max_hold` clamp rather than when its bucket
876 /// would have allowed it. Report `Impairment{HoldClamped}`, whose
877 /// cardinality is already *once per clamped unit*.
878 Clamped {
879 /// What the bucket asked for, or `None` when it named no instant at
880 /// all — a zero rate, or a unit larger than `burst_bytes`, where the
881 /// wait is genuinely unbounded.
882 ///
883 /// `None` rather than a sentinel duration: a finite figure here
884 /// would be a fabrication, and the largest representable one renders
885 /// as an eighteen-quintillion-second request that reads as an
886 /// encoding fault rather than as "unbounded".
887 requested: Option<Duration>,
888 /// What was applied: the clamp.
889 applied: Duration,
890 },
891 /// A unit outlived its clamp under `Expiry::ResetStream`, and the queue
892 /// has replaced everything with the reset. Report exactly one
893 /// `Shaped{Expired}`.
894 Expired,
895 /// The head could not be granted because its class's `burst_bytes` is
896 /// smaller than the head itself, so that class's configured rate can
897 /// never pace anything. Report `Impairment{ShapeBurstBelowUnit}`.
898 ///
899 /// The queue produces this on **every** refusal of such a unit and does
900 /// not deduplicate: the burst is a property of the session's profile,
901 /// not of this stream, so the report-once state belongs to the scheduler
902 /// and the caller claims it there through
903 /// `Scheduler::claim_burst_report`. A per-queue latch would report once
904 /// per stream for a fault that is the same fault on every stream.
905 ///
906 /// Carries the class rather than its name so this type stays `Copy`;
907 /// resolving a name here would allocate on a release path for an event
908 /// that is emitted once.
909 BurstBelowUnit {
910 /// The class whose rate is not being applied.
911 class: Class,
912 /// Its bucket cap, as configured.
913 burst_bytes: u64,
914 /// The wire size of the unit the cap could not cover.
915 unit_bytes: u64,
916 },
917}
918
919impl PendingQueue {
920 /// An empty queue. Allocates nothing until the first [`Self::push`].
921 pub(crate) fn new(config: EgressConfig, counters: Arc<Recorder>) -> Self {
922 Self {
923 q: VecDeque::new(),
924 head_deadline: None,
925 queued_bytes: 0,
926 flushed_unconfirmed: 0,
927 gauge: None,
928 shape_hold: config.max_hold,
929 config,
930 counters,
931 backpressure_reported: false,
932 shape_depth: None,
933 shaper: None,
934 shape_stats: None,
935 shape_side: None,
936 unit_class: Class::Unshapeable,
937 shape_gate: None,
938 shape_parked: false,
939 shape_demand: None,
940 tokens_dry: false,
941 starved_from: 1,
942 shape_report: None,
943 }
944 }
945
946 /// Report this queue's depth into its session's [`EgressGauge`].
947 ///
948 /// A builder for the same reason [`Self::with_shape_depth`] is: the
949 /// tests in this module build queues that belong to no session, and a
950 /// required parameter would make each of them invent one.
951 /// Installed by every pipe function, on every stream, shaped or not —
952 /// unlike a scheduler, which only the framed data pipe installs. A gauge
953 /// that only some queues reported into would answer *the session has
954 /// nothing left to flush* while a control stream still held a deferred
955 /// SUBSCRIBE_OK.
956 pub(crate) fn with_gauge(mut self, gauge: Arc<EgressGauge>) -> Self {
957 // A fresh queue is empty, so there is nothing to charge here. If
958 // that ever stops being true this has to charge `queued_bytes`.
959 debug_assert_eq!(self.queued_bytes, 0, "a queue takes its gauge before it takes bytes");
960 self.gauge = Some(gauge);
961 self
962 }
963
964 /// Add `bytes` to this queue's depth and to its session's gauge.
965 fn charge(&mut self, bytes: usize) {
966 self.queued_bytes = self.queued_bytes.saturating_add(bytes);
967 if let Some(gauge) = &self.gauge {
968 gauge.add(bytes);
969 }
970 }
971
972 /// Take `bytes` off this queue's depth and off its session's gauge.
973 fn credit(&mut self, bytes: usize) {
974 let bytes = bytes.min(self.queued_bytes);
975 self.queued_bytes -= bytes;
976 if let Some(gauge) = &self.gauge {
977 gauge.sub(bytes);
978 }
979 }
980
981 /// Whether this queue's session has stopped flushing at teardown.
982 ///
983 /// `false` for a queue with no gauge, which is every queue built
984 /// outside a session: nothing can have asked such a queue to stop.
985 fn discarding(&self) -> bool {
986 self.gauge.as_ref().is_some_and(|g| g.is_discarding())
987 }
988
989 /// Carry a shaping profile's per-stream depth into [`Self::accepts_more`].
990 ///
991 /// A builder rather than a fourth parameter on [`Self::new`], because
992 /// every caller but one passes `None` and the shape of the exception is
993 /// the point: exactly one construction site — the framed data pipe —
994 /// has a profile to install, and the ~25 unit tests below construct
995 /// queues that never had one.
996 ///
997 /// **Installed only under `Overflow::Block`**, which the caller
998 /// decides; this type deliberately does not name `Overflow`. What it
999 /// does is enforce a depth, and a depth that reaches it is one the
1000 /// session has already decided should stop reads.
1001 ///
1002 /// Plumbed *into* `accepts_more` rather than checked beside it because
1003 /// the once-per-stream backpressure latch is computed inside
1004 /// [`Self::push`] from `accepts_more()`: a stream full by
1005 /// `QueueConfig::depth_objects` but under
1006 /// [`EgressConfig::max_pending_bytes`] would otherwise never trip it,
1007 /// and `Impairment{EgressQueueFull}` would have no producer on a shaped
1008 /// stream at all.
1009 pub(crate) fn with_shape_depth(mut self, depth: Option<QueueDepth>) -> Self {
1010 self.shape_depth = depth;
1011 self
1012 }
1013
1014 /// Install the session's shaper, making this a **paced** queue.
1015 /// A builder for the same reason [`Self::with_shape_depth`] is: exactly one
1016 /// construction site — the framed data pipe — has a shaper, and the ~25
1017 /// unit tests below build queues that never had one. Both control pipes and
1018 /// `pipe_data_passthrough` call [`Self::new`] and stop there, which is what
1019 /// makes *the control pipes are never shaped* a property of the type graph
1020 /// rather than of a review.
1021 ///
1022 /// What it changes, and only this: [`Self::pop_next_due`] asks
1023 /// `Scheduler::acquire` before it yields a shaped unit, and [`Self::push`]
1024 /// tags what it queues. Everything else — ordering, `due_at`, `Hold`
1025 /// gates, the two drains, `head_release`'s signature — is untouched.
1026 ///
1027 /// `side` travels with the recorder rather than beside it because the
1028 /// recorder is session-scoped and this queue is not: one `ShapeRecorder`
1029 /// is shared by the forwarding tasks of both legs, and the figures it
1030 /// keeps are per leg and per direction. Naming it here — at the one site
1031 /// that has both a profile and a stream — is what makes a downlink stall
1032 /// unattributable to the uplink.
1033 ///
1034 /// It is the side this stream's traffic **arrives** on, which is the
1035 /// only side a forwarding task is ever handed. What a release figure
1036 /// does with it — charge the leg the unit leaves by — is the recorder's
1037 /// business, not this queue's.
1038 pub(crate) fn with_shaper(
1039 mut self,
1040 shaper: Option<Arc<Scheduler>>,
1041 stats: Arc<ShapeRecorder>,
1042 side: ProxySide,
1043 ) -> Self {
1044 if let Some(shaper) = shaper {
1045 self.shape_hold = shaper.max_hold().unwrap_or(self.config.max_hold);
1046 self.shaper = Some(shaper);
1047 self.shape_stats = Some(stats);
1048 self.shape_side = Some(side);
1049 }
1050 self
1051 }
1052
1053 /// Whether this queue paces what it holds.
1054 ///
1055 /// Read by `exec::Engine::queue_is_busy`, which is what routes a shaped
1056 /// stream's units *through* the queue instead of writing them inline: a
1057 /// unit written straight to the transport never reaches
1058 /// [`Self::pop_next_due`], so on a shaped stream the fast path is exactly
1059 /// the path that cannot be paced.
1060 pub(crate) fn is_shaped(&self) -> bool {
1061 self.shaper.is_some()
1062 }
1063
1064 /// Name the class of the unit the pipe loop is about to queue.
1065 ///
1066 /// Set once per framed unit, immediately after classification, and read
1067 /// by every [`Self::push`] until the next unit overwrites it. That is why
1068 /// `exec` can queue a `Delay`'d object without ever naming a class: the
1069 /// tag is the loop's, the push is `exec`'s, and neither has to know about
1070 /// the other.
1071 ///
1072 /// A no-op on an unshaped queue, where nothing reads it.
1073 pub(crate) fn tag_unit(&mut self, class: Class) {
1074 self.unit_class = class;
1075 }
1076
1077 /// Whatever the last [`Self::pop_next_due`] wants the caller to report.
1078 pub(crate) fn take_shape_report(&mut self) -> Option<ShapeReport> {
1079 self.shape_report.take()
1080 }
1081
1082 /// Whether anything is waiting.
1083 pub(crate) fn is_empty(&self) -> bool {
1084 self.q.is_empty()
1085 }
1086
1087 /// How many units are waiting.
1088 ///
1089 /// Read by this module's tests and by `exec.rs`'s, which assert
1090 /// `DeferredEffects::len() == PendingQueue::len()`, and on the data
1091 /// path by shaping admission, which needs the object count the
1092 /// arriving unit would be the `n + 1`-th of. The pipe loops themselves
1093 /// ask [`Self::accepts_more`] and [`Self::is_empty`].
1094 pub(crate) fn len(&self) -> usize {
1095 self.q.len()
1096 }
1097
1098 /// Bytes the queue is holding, right now, still unwritten.
1099 ///
1100 /// Not the teardown report — see [`Self::unconfirmed_bytes`]. This is
1101 /// the live figure the byte budget and the tests are written against.
1102 pub(crate) fn queued_bytes(&self) -> usize {
1103 self.queued_bytes
1104 }
1105
1106 /// Bytes this queue cannot vouch for: everything a teardown drain
1107 /// handed to the transport, **plus** everything still queued.
1108 ///
1109 /// What `Impairment { QueuedBytesAtTeardown }` carries, and the reason
1110 /// it is not simply [`Self::queued_bytes`]. A cancelled session's pipe
1111 /// task races `run_with_transport`'s `client.close()` / `relay.close()`:
1112 /// [`Self::drain_ignoring_release_times`] can hand a 256 KiB
1113 /// object to quinn, have `write_all` return `Ok` because quinn buffered
1114 /// it, and have the connection torn down before a byte of it is on the
1115 /// wire. Reporting only the residue reports **zero** there, and the
1116 /// object is gone with no event at all — the one failure class this
1117 /// engine forbids outright: traffic is never silently gone.
1118 ///
1119 /// So a teardown flush counts as *unconfirmed*, not as delivered. The
1120 /// event says exactly this already: "they were flushed best-effort;
1121 /// `bytes` may not have reached the peer". A peer that did receive them
1122 /// gets a spurious impairment, which is the right side to err on: an
1123 /// over-report is a diagnosable nuisance, a silent loss is not.
1124 ///
1125 /// Zero on every path that is not a teardown, so a stream that drains
1126 /// at its release times reports nothing.
1127 pub(crate) fn unconfirmed_bytes(&self) -> usize {
1128 self.flushed_unconfirmed.saturating_add(self.queued_bytes)
1129 }
1130
1131 /// The engine knobs this queue was built with.
1132 pub(crate) fn config(&self) -> &EgressConfig {
1133 &self.config
1134 }
1135
1136 /// Whether the source may still be **read from**.
1137 ///
1138 /// `false` once the queue holds [`EgressConfig::max_pending_bytes`] or
1139 /// [`MAX_PENDING_UNITS`] units — the point at which `Delay` stops being
1140 /// a latency shift and becomes backpressure — **or** once it reaches a
1141 /// shaping profile's own depth, when one was installed by
1142 /// [`Self::with_shape_depth`]. A `bool`, deliberately: it is hoisted
1143 /// out of the `select!` and holds no borrow.
1144 ///
1145 /// **Read from, not observed.** This gates `recv.read`, which is the
1146 /// only call that consumes bytes and therefore the only one that grants
1147 /// the peer flow-control credit. It does *not* gate whether the pipe
1148 /// loop watches its source: `false` swaps the read for
1149 /// `RecvStream::received_reset`, so a peer's `RESET_STREAM` is mirrored
1150 /// at once however full this queue is. Gating the observation on this
1151 /// was the defect — a dry bucket under `Overflow::Block` held the read
1152 /// shut for `max_hold` (30 s by default) and the reset went unseen for
1153 /// the whole of it.
1154 ///
1155 /// Both terms are **per stream**, and both imply a non-empty deque, so
1156 /// `head_release()` stays `Some` and the release branch stays live. A
1157 /// shared cross-class budget must never reach this function: an empty
1158 /// own queue with a shared budget exhausted disables the read branch
1159 /// *and* the release branch, and the stream deadlocks until session
1160 /// teardown.
1161 pub(crate) fn accepts_more(&self) -> bool {
1162 let within_engine_budget =
1163 self.queued_bytes < self.config.max_pending_bytes && self.q.len() < MAX_PENDING_UNITS;
1164 let within_shape_depth = match self.shape_depth {
1165 Some(depth) => self.queued_bytes < depth.bytes && self.q.len() < depth.objects,
1166 None => true,
1167 };
1168 within_engine_budget && within_shape_depth
1169 }
1170
1171 /// What the head is waiting for, as an owned clone — see [`Release`].
1172 ///
1173 /// **`&self`, and it debits nothing.** It reports the deadline the
1174 /// scheduler last computed and the gate it last parked on; the decision
1175 /// itself belongs to [`Self::pop_next_due`], which is `&mut self` and
1176 /// runs exactly once per released unit. This function is called once
1177 /// per `select!` iteration from a hoisted local, so a debit here would
1178 /// be charged per read-wake rather than per byte written.
1179 ///
1180 /// **The FIN drain *is* on `pop_next_due`'s path.**
1181 /// [`drain_honouring_release_times`] —
1182 /// the read arm's `None` branch — loops `pop_next_due`, debits the
1183 /// bucket, and applies the clamp and the expiry, which is why it takes
1184 /// an `on_shape` callback and reports them. The control pipes really
1185 /// are exempt, but by *construction* rather than by call graph: they
1186 /// install no [`Scheduler`] at all, so
1187 /// `shaping_grants_head` short-circuits on `shaper: None` and no
1188 /// control frame can reach a bucket even by accident. The only drain
1189 /// that is genuinely off the seam is
1190 /// [`Self::drain_ignoring_release_times`], which pops the front
1191 /// directly and consults nothing.
1192 pub(crate) fn head_release(&self) -> Option<Release> {
1193 let head = self.q.front()?;
1194 debug_assert!(
1195 self.head_deadline.is_some(),
1196 "a non-empty queue always has an armed head deadline",
1197 );
1198 Some(Release {
1199 deadline: self.head_deadline.clone()?,
1200 // Exactly one of the two, never both, and the choice is the spin
1201 // guard `shape_parked` documents: while the shaper is holding a
1202 // unit the shaper's gate is the only one that can usefully wake
1203 // it, because the unit's own `Hold` gate has already fired and
1204 // would resolve `wait_release` on every iteration forever.
1205 gate: if self.shape_parked { self.shape_gate.clone() } else { head.gate.clone() },
1206 })
1207 }
1208
1209 /// When the queue expects to have written the last unit in it, or
1210 /// `None` if it is empty.
1211 ///
1212 /// `None` rather than `Instant::now()`: the estimate exists only to say
1213 /// what a unit waits behind, and on an empty queue there is nothing
1214 /// ahead. Folding `now` in there would quietly shift every unit's
1215 /// reported release time forward to its push instant.
1216 fn tail_expected_at(&self) -> Option<Instant> {
1217 self.q.back().map(|u| u.expected_at)
1218 }
1219
1220 /// Register the head with the release wheel.
1221 ///
1222 /// Armed at the head's **`due_at`**, never its `expected_at`: the head
1223 /// has nothing ahead of it, so its own deadline is when it may go, and
1224 /// arming on the estimate would re-introduce the successor stall the
1225 /// module doc describes (a unit clamped behind a `Hold` would sleep to
1226 /// that hold's ceiling even after it became the head).
1227 ///
1228 /// The **only** caller of [`release_timer::arm_at`] besides
1229 /// [`Self::push`]'s use of it through here. `arm_at` starts nothing when
1230 /// the head is already due, so a queue of undelayed units never
1231 /// constructs the wheel.
1232 fn arm_head(&mut self) {
1233 self.head_deadline = self.q.front().map(|u| release_timer::arm_at(u.due_at));
1234 }
1235
1236 /// Re-arm the head at an instant the *scheduler* named, rather than at
1237 /// the head's own `due_at`.
1238 /// The head is due on its own account — that is how it reached the shaper —
1239 /// so re-arming at `due_at` would give an already-fired deadline and spin
1240 /// the release branch hot until the bucket refilled. This is what makes
1241 /// `head_release()` *the deadline the scheduler last computed*: it reads
1242 /// back exactly what was armed here.
1243 fn arm_head_at(&mut self, at: Instant) {
1244 self.head_deadline = Some(release_timer::arm_at(at));
1245 }
1246
1247 /// Queue a unit behind everything already waiting.
1248 ///
1249 /// **`due_at` is not touched.** The unit keeps the deadline it was
1250 /// built with, because that deadline is its own: a `Delay`'s
1251 /// `arrived_at + by`, a `Hold`'s `max_hold` ceiling, or `now` for a
1252 /// unit queued purely for ordering. Wire order is held by the deque —
1253 /// [`Self::pop_next_due`] only ever considers the *front* — so nothing
1254 /// has to be added to a successor's deadline to keep it behind its
1255 /// predecessor, and adding something is exactly what stranded a stream
1256 /// behind a released `Hold` (module doc).
1257 /// What *is* computed here is [`Pending::expected_at`], the queue's
1258 /// estimate of when this unit will reach the wire: `max(due_at,
1259 /// tail.expected_at, now)` on a non-empty queue, and `due_at` on an empty
1260 /// one. It is what [`Push::release_at`] returns, so `Effect::Queued {
1261 /// release_at }` still says *no earlier than everything ahead of you*, and
1262 /// it is what [`Self::record_release`] measures lateness against, so a
1263 /// `Pass` queued behind a 300 ms `Delay` is not logged as a 300 ms release
1264 /// error.
1265 ///
1266 /// A unit that is due now on an *empty* queue should not be pushed at
1267 /// all — the pipe loop writes it inline in the read arm, which is
1268 /// today's code and today's cost.
1269 pub(crate) fn push(&mut self, mut unit: Pending) -> Push {
1270 if let Some(tail) = self.tail_expected_at() {
1271 unit.expected_at = unit.due_at.max(tail).max(Instant::now());
1272 }
1273 // Tagged here, not at the call site, so that every producer of a
1274 // queued unit — this file's own terminals, `exec`'s `Delay`/`Hold`
1275 // pushes, and the pipe loop's own header and passthrough bytes — is
1276 // charged to the class the loop resolved, without any of them naming
1277 // one. The clock read is behind the shaping test, so an unshaped push
1278 // costs exactly what it did before.
1279 if self.shaper.is_some() {
1280 unit.shape = Some(ShapeTag {
1281 class: self.unit_class,
1282 expires_at: Instant::now() + self.shape_hold,
1283 starved_noted: false,
1284 });
1285 self.note_unshapeable_seen(&unit);
1286 }
1287 let release_at = unit.expected_at;
1288 let becomes_head = self.q.is_empty();
1289 self.charge(unit.len());
1290 self.q.push_back(unit);
1291 self.counters.note_egress_item_queued();
1292 if becomes_head {
1293 self.arm_head();
1294 }
1295 self.sync_shape_demand();
1296 let entered_backpressure = !self.accepts_more() && !self.backpressure_reported;
1297 if entered_backpressure {
1298 self.backpressure_reported = true;
1299 }
1300 Push { release_at, entered_backpressure }
1301 }
1302
1303 /// Pop the head if it is due at `now`, re-arming the new head.
1304 ///
1305 /// **The front and only the front.** That single fact is the whole
1306 /// ordering guarantee: a unit whose own deadline passed long ago is
1307 /// still not popped while anything is queued in front of it.
1308 ///
1309 /// Call it in a `while let` loop: `pop_next_due` yields one unit at a
1310 /// time and allocates nothing, where a `Vec`-returning `pop_due` would
1311 /// allocate on every release wake. Looping matters as much as it reads:
1312 /// one wake on a `Hold`'s gate has to drain the held unit *and* every
1313 /// unit behind it that is due on its own account, or releasing a gate
1314 /// resumes one object and strands the rest.
1315 ///
1316 /// # The release seam
1317 ///
1318 /// On a shaped queue — and only there — a unit that is due on its own
1319 /// account is then put to `Scheduler::acquire`, which debits its class's
1320 /// token bucket and applies the [`Discipline`]. This function is where
1321 /// that happens because it is `&mut self`, it is the sole ordering
1322 /// authority, and it is called **exactly once per released unit**;
1323 /// `head_release` is none of those things.
1324 ///
1325 /// A refusal is not an error and costs nothing: the head deadline is
1326 /// re-armed at whatever the scheduler named (clamped by the unit's own
1327 /// `max_hold`), the gate a discipline handed back is stored for
1328 /// `head_release`, and `None` is returned exactly as a not-yet-due head
1329 /// returns it. The caller's loop is unchanged.
1330 ///
1331 /// [`Discipline`]: crate::shape::Discipline
1332 pub(crate) fn pop_next_due(&mut self, now: Instant) -> Option<Pending> {
1333 if !self.q.front().is_some_and(|u| u.is_due(now)) {
1334 return None;
1335 }
1336 // A fresh decision every call: a gate stored by a previous refusal
1337 // says nothing about this one, and leaving it set would let
1338 // `head_release` hand out a gate nobody will release.
1339 self.shape_gate = None;
1340 self.shape_parked = false;
1341 if !self.shaping_grants_head(now) {
1342 return None;
1343 }
1344 let unit = self.q.pop_front()?;
1345 self.credit(unit.len());
1346 self.starved_from = self.starved_from.saturating_sub(1).max(1);
1347 self.note_delivered(&unit);
1348 self.arm_head();
1349 self.sync_shape_demand();
1350 Some(unit)
1351 }
1352
1353 /// Whether the shaper lets the head go at `now`, arming whatever it must
1354 /// wait for when it does not.
1355 ///
1356 /// `true` on every unshaped queue and for every unit the shaper does not
1357 /// own — a terminal, an elided ordering slot, a header — so the only
1358 /// thing a bucket can ever hold back is bytes.
1359 fn shaping_grants_head(&mut self, now: Instant) -> bool {
1360 let Some(shaper) = self.shaper.clone() else {
1361 return true;
1362 };
1363 let Some(head) = self.q.front() else {
1364 return true;
1365 };
1366 let Some(tag) = head.shape else {
1367 return true;
1368 };
1369 // Terminals bypass the pacer. A queued reset or truncate is the end
1370 // of the stream, not media: gating it would make teardown ordering
1371 // depend on a token bucket, which is exactly what turns the
1372 // data-then-reset tests into coin flips.
1373 if matches!(head.item, Item::Terminal(_)) {
1374 return true;
1375 }
1376 let bytes = head.len() as u64;
1377
1378 // The clamp comes first, and it is what makes starvation late rather
1379 // than lossy: a unit that has outlived `max_hold` is decided by
1380 // `Expiry`, never by a bucket that has already refused it.
1381 if now >= tag.expires_at {
1382 return self.expire_head(shaper.on_expiry());
1383 }
1384
1385 match shaper.acquire(tag.class, bytes, now) {
1386 Acquire::Now => {
1387 self.tokens_dry = false;
1388 true
1389 }
1390 Acquire::Later(at) => {
1391 self.note_tokens_dry(tag.class);
1392 self.park_head(at.min(tag.expires_at), tag, None);
1393 false
1394 }
1395 // No refill instant exists, so the clamp *is* the deadline. Arming
1396 // a fabricated far-future instant instead would arm a timer that
1397 // never fires, and the stream would look identical to one that
1398 // had simply hung.
1399 Acquire::Never => {
1400 self.note_tokens_dry(tag.class);
1401 self.park_head(tag.expires_at, tag, None);
1402 false
1403 }
1404 // The clamp is the deadline here too — nothing about *time* makes
1405 // a unit larger than the whole bucket fit — but this refusal is a
1406 // mis-sized burst rather than a rate doing its job, and the two
1407 // are otherwise identical from outside: same clamp, same counter,
1408 // same `HoldClamped`. The report is the only thing that separates
1409 // them, so it is set here and deduplicated by the scheduler.
1410 Acquire::LargerThanBurst { burst_bytes, unit_bytes } => {
1411 self.note_tokens_dry(tag.class);
1412 self.shape_report =
1413 Some(ShapeReport::BurstBelowUnit { class: tag.class, burst_bytes, unit_bytes });
1414 self.park_head(tag.expires_at, tag, None);
1415 false
1416 }
1417 Acquire::Starved(gate) => {
1418 self.park_head(tag.expires_at, tag, Some(gate));
1419 false
1420 }
1421 }
1422 }
1423
1424 /// Charge one **episode** of this class's own bucket being dry, on the
1425 /// edge only.
1426 ///
1427 /// Shared by the three refusals that are the bucket's rather than a
1428 /// discipline's, so the edge latch cannot be re-armed on one of them and
1429 /// forgotten on another. Re-armed by a grant, in `shaping_grants_head`.
1430 fn note_tokens_dry(&mut self, class: Class) {
1431 if self.tokens_dry {
1432 return;
1433 }
1434 self.tokens_dry = true;
1435 if let Some(stats) = &self.shape_stats {
1436 stats.note_tokens_exhausted(class);
1437 }
1438 }
1439
1440 /// Park the head until `at` — or until `gate` opens, when a discipline is
1441 /// what is holding it — and charge everything queued behind a unit of
1442 /// another class.
1443 fn park_head(&mut self, at: Instant, tag: ShapeTag, gate: Option<Gate>) {
1444 let by_discipline = gate.is_some();
1445 self.shape_gate = gate;
1446 self.shape_parked = true;
1447 self.arm_head_at(at);
1448 self.sync_shape_demand();
1449 if by_discipline {
1450 // The head itself waited on **another class**, not on its own
1451 // bucket. Same counter as a same-stream head of another class,
1452 // because to a caller they are one question — *was I held
1453 // up by my rate, or by somebody else?* — and the answer is the same
1454 // in both. What it is emphatically not is
1455 // `tokens_exhausted_episodes`: the bucket was never asked.
1456 self.note_head_starved();
1457 }
1458 self.note_starved_behind(tag.class);
1459 }
1460
1461 /// Charge the head once against `starved_behind_other_class`.
1462 fn note_head_starved(&mut self) {
1463 let Some(stats) = self.shape_stats.clone() else { return };
1464 let Some(tag) = self.q.front_mut().and_then(|u| u.shape.as_mut()) else { return };
1465 if tag.starved_noted {
1466 return;
1467 }
1468 tag.starved_noted = true;
1469 stats.note_starved(tag.class);
1470 }
1471
1472 /// Charge `starved_behind_other_class` for every queued unit that is
1473 /// waiting behind a unit of a *different* class.
1474 ///
1475 /// Once per unit over the queue's whole life, not once per wake: the
1476 /// sweep resumes from where the last one stopped and the cursor follows
1477 /// the front as units are popped, so the total work is linear in units
1478 /// queued rather than quadratic in wakes.
1479 ///
1480 /// Deliberately separate from `tokens_exhausted_episodes`. Head-gating
1481 /// makes configured shaping and head-of-line blocking look identical from
1482 /// outside, and this is the number that tells them apart.
1483 fn note_starved_behind(&mut self, head_class: Class) {
1484 let Some(stats) = self.shape_stats.clone() else { return };
1485 let len = self.q.len();
1486 for index in self.starved_from.max(1)..len {
1487 let Some(unit) = self.q.get_mut(index) else { continue };
1488 let Some(tag) = unit.shape.as_mut() else { continue };
1489 if tag.starved_noted || tag.class == head_class {
1490 continue;
1491 }
1492 tag.starved_noted = true;
1493 stats.note_starved(tag.class);
1494 }
1495 self.starved_from = len.max(1);
1496 }
1497
1498 /// The head outlived its clamp. Deliver it, or abandon the stream.
1499 ///
1500 /// Returns whether the caller may pop the head. Under
1501 /// [`Expiry::Deliver`] it may — that is the whole of "clamps and
1502 /// delivers", and it is why `objects_expired` is zero by default.
1503 fn expire_head(&mut self, expiry: Expiry) -> bool {
1504 match expiry {
1505 Expiry::Deliver => {
1506 // What the bucket *would* have asked for is gone by now — the
1507 // deadline it named is in the past, or it named none — so the
1508 // report states the clamp that was applied and leaves the
1509 // request absent rather than inventing a figure for it.
1510 self.shape_report =
1511 Some(ShapeReport::Clamped { requested: None, applied: self.shape_hold });
1512 true
1513 }
1514 Expiry::ResetStream { code } => {
1515 if let (Some(stats), Some(side)) = (&self.shape_stats, self.shape_side) {
1516 stats.note_expired(side);
1517 stats.note_stream_reset_by_shaping(side);
1518 }
1519 self.shape_report = Some(ShapeReport::Expired);
1520 // Everything queued is replaced by the reset: the stream is
1521 // being given up on, and writing part of it first would leave
1522 // the peer a prefix it cannot distinguish from a truncation.
1523 self.clear();
1524 self.q.push_back(Pending::terminal(Terminal::Reset { code }));
1525 self.arm_head();
1526 true
1527 }
1528 }
1529 }
1530
1531 /// Account for a queued unit **no rule could see**: the left-hand side of
1532 /// the conservation identity, for bytes the classifier never met.
1533 /// A stream header, an oversized object's passthrough chunk and a bypassed
1534 /// stream's tail carry no `ObjectMeta`, so `note_object_seen` is never
1535 /// reached for them and `bytes_shaped` would not count them. They are still
1536 /// bytes the shaper handled — it queued them, it ordered them, and it wrote
1537 /// them — so leaving them out would make `bytes_shaped` mean "bytes a rule
1538 /// saw" while
1539 /// [`ShapeStats::bytes_shaped`](crate::shape::ShapeStats::bytes_shaped)
1540 /// promises *every byte it accounted for*, and the `unshapeable` row would
1541 /// have nothing to be the other half of.
1542 ///
1543 /// **Here and not at the call site**, which is what makes the identity
1544 /// structural: `push` is the one place an unshapeable unit enters, this
1545 /// reads the same `unit.len()` [`Self::note_delivered`] will charge when
1546 /// it leaves, and both are gated on the same `ShapeTag`. `session.rs`
1547 /// cannot get it wrong because `session.rs` is not asked — the object
1548 /// arm and the unshown arm share one `exec` entry point, and only the
1549 /// tag tells them apart.
1550 ///
1551 /// Zero-length units — an elided ordering slot, a queued reset — add
1552 /// nothing on either side.
1553 fn note_unshapeable_seen(&self, unit: &Pending) {
1554 if self.unit_class != Class::Unshapeable {
1555 return;
1556 }
1557 let (Some(stats), Some(side)) = (&self.shape_stats, self.shape_side) else {
1558 return;
1559 };
1560 let bytes = unit.len();
1561 if bytes > 0 {
1562 stats.note_unshapeable_seen(side, bytes as u64);
1563 }
1564 }
1565
1566 /// Charge one released unit to its class.
1567 ///
1568 /// [`Class::Unshapeable`] is charged like any other row, and only
1569 /// because [`Self::note_unshapeable_seen`] now counts the same bytes
1570 /// into `bytes_shaped` on the way in: both terms move together or
1571 /// neither does, and this is the unit in which they moved.
1572 /// The consequence worth stating: unshapeable bytes are **not paced**.
1573 /// `shaping_grants_head` never asks a bucket about them — they carry no
1574 /// class a bucket is configured for — so an object too large for the framer
1575 /// to buffer bypasses every rate, and a class rate can be exceeded by
1576 /// exactly one oversized object. That is what "unshapeable" means; the row
1577 /// named for it is where the figure is reported, and it is deliberately a
1578 /// *separate* row from `default_class` so that *no rule claimed this unit*
1579 /// and "no rule could have" stay two answers.
1580 ///
1581 /// The row alone does not say *whose* rate went, which is the question an
1582 /// author with a 500 kbps video cap and a large initial segment is
1583 /// actually asking, so the pipe loop pairs it with
1584 /// `Impairment{ShapeUnpacedObject}` naming the class the stream's
1585 /// classified units are charged to.
1586 fn note_delivered(&self, unit: &Pending) {
1587 let (Some(stats), Some(side), Some(tag)) = (&self.shape_stats, self.shape_side, unit.shape)
1588 else {
1589 return;
1590 };
1591 let bytes = unit.len();
1592 if bytes > 0 {
1593 stats.note_delivered(side, tag.class, bytes as u64);
1594 }
1595 }
1596
1597 /// Tell the scheduler which class this queue is holding an unreleased
1598 /// head of, if any.
1599 ///
1600 /// **Demand, not depth.** One declaration per queue, replaced whenever
1601 /// the head's class changes and withdrawn when the queue empties — which
1602 /// includes [`Self::clear`], the teardown drain, and [`Drop`]. A
1603 /// declaration that outlived its stream would starve a lower-priority
1604 /// class for the rest of the session, and the failure would look like a
1605 /// hang rather than like a leak.
1606 fn sync_shape_demand(&mut self) {
1607 let Some(shaper) = &self.shaper else { return };
1608 let want = self.q.front().and_then(|u| u.shape).map(|t| t.class);
1609 if want == self.shape_demand {
1610 return;
1611 }
1612 if let Some(old) = self.shape_demand.take() {
1613 shaper.withdraw_demand(old);
1614 }
1615 if let Some(new) = want {
1616 shaper.declare_demand(new);
1617 self.shape_demand = Some(new);
1618 }
1619 }
1620
1621 /// Sample one deferred release's lateness onto the session counters.
1622 ///
1623 /// Call it **only** from the release branch, once per unit
1624 /// `pop_next_due` yields, before writing. Units written inline in the
1625 /// read arm are not releases and would dilute the distribution with
1626 /// zeros; drains ignore release times by design and recording their
1627 /// lateness would report teardown as a timing failure. Neither
1628 /// drain in this file calls it.
1629 /// Measured against [`Pending::expected_at`], not [`Pending::due_at`]: the
1630 /// question is *did the engine release when it said it would*, and what it
1631 /// said was `Effect::Queued { release_at }`. Sampling `due_at` instead
1632 /// would log the queueing delay of every unit behind a `Delay` as a timer
1633 /// error.
1634 ///
1635 /// `saturating_duration_since` because a unit released early — a
1636 /// `Hold`'s gate opening long before its ceiling, which is the normal
1637 /// case — is a zero, not a negative.
1638 pub(crate) fn record_release(&self, unit: &Pending, now: Instant) {
1639 self.counters.record_release(now.saturating_duration_since(unit.expected_at));
1640 }
1641
1642 /// Forget everything queued. Used after a terminal fires — the
1643 /// destination is reset, so nothing behind it can be written.
1644 ///
1645 /// Dropping the [`Release`]s abandons their wheel slots, which the
1646 /// wheel prunes at their own instants or at a sweep, without waking
1647 /// anything.
1648 pub(crate) fn clear(&mut self) {
1649 self.q.clear();
1650 self.credit(self.queued_bytes);
1651 self.head_deadline = None;
1652 self.starved_from = 1;
1653 // The shaping gate goes with the units it was holding, and the
1654 // demand goes with the head that declared it: a queue that is being
1655 // emptied has nothing to send, and leaving the declaration standing
1656 // would starve a lower class for the rest of the session.
1657 self.shape_gate = None;
1658 self.shape_parked = false;
1659 self.sync_shape_demand();
1660 }
1661
1662 /// Write everything queued **now**, in order, ignoring release times.
1663 ///
1664 /// The teardown path: delivered late beats lost silently. Best effort
1665 /// by construction — a write failure stops the drain and leaves the
1666 /// failing unit and everything behind it queued, so
1667 /// [`Self::queued_bytes`] reports exactly what did not reach the
1668 /// transport.
1669 ///
1670 /// Every byte it *does* hand over is added to
1671 /// [`Self::unconfirmed_bytes`], because a `write_all` that returns `Ok`
1672 /// into a connection another task is about to `close()` is not a
1673 /// delivery. That is the whole of the teardown-race loss: read
1674 /// [`Self::unconfirmed_bytes`] after this, never [`Self::queued_bytes`],
1675 /// on any path that runs because the session is going down.
1676 ///
1677 /// **Consults no bucket.** This is the teardown drain, and teardown
1678 /// bypasses the pacer entirely: it pops the front directly
1679 /// rather than through [`Self::pop_next_due`], so a token bucket can
1680 /// never gate a mirrored reset. That is what keeps the eleven
1681 /// data-then-teardown ordering tests independent of any configured rate.
1682 ///
1683 /// **And therefore reports no shaping, deliberately.** Every other
1684 /// release path takes a [`ShapeReport`] out of the queue and hands it to
1685 /// a reporter, because a clamp or an expiry that nothing says happened is
1686 /// indistinguishable from a profile that did nothing. This one is the
1687 /// exception, and it is an exception because it *applies* no shaping:
1688 /// [`Self::pop_next_due`] is the only producer of a `ShapeReport` and
1689 /// this function never calls it, so there is nothing to drain rather
1690 /// than something dropped. The slot is empty on entry as well — both
1691 /// callers that can reach it through a shaped queue
1692 /// ([`drain_honouring_release_times`]'s cancel arm and
1693 /// `release_due_units`) drain the report before the pop that produced it
1694 /// is written.
1695 ///
1696 /// **Known residue, stated rather than fixed:** popping the front
1697 /// directly also skips the `shape_parked` / `shape_gate` bookkeeping
1698 /// [`Self::pop_next_due`] maintains, so after this drain a
1699 /// [`Self::head_release`] would describe a park belonging to a unit that
1700 /// is gone. Nothing reads it — every caller returns immediately, the
1701 /// queue is empty or cleared, and `head_release` answers `None` on an
1702 /// empty deque — so there is no failure case here to fix. It is
1703 /// written down because "unreachable today" is a property of the
1704 /// callers, not of this function.
1705 pub(crate) async fn drain_ignoring_release_times<S: EgressSink>(
1706 &mut self,
1707 send: &mut S,
1708 ) -> DrainOutcome {
1709 // A close that was given a drain window and spent it has already
1710 // decided these bytes are not going out. Writing them anyway would move
1711 // them from *queued, and reported as abandoned* into *handed to a
1712 // connection that is closing*, which is the one state nothing
1713 // downstream can count: see [`EgressGauge`].
1714 if self.discarding() {
1715 return DrainOutcome::Discarded;
1716 }
1717 while let Some(unit) = self.q.front().cloned() {
1718 match write_unit(unit, send).await {
1719 Ok(Written::Terminated { forwarded, code }) => {
1720 // Everything behind a terminal is discarded by design,
1721 // not lost: the destination is reset. Only the prefix
1722 // this unit actually handed over is unconfirmed.
1723 self.flushed_unconfirmed = self.flushed_unconfirmed.saturating_add(forwarded);
1724 self.clear();
1725 return DrainOutcome::Terminated { forwarded, code };
1726 }
1727 Ok(written) => {
1728 if let Written::Bytes(n) = written {
1729 self.flushed_unconfirmed = self.flushed_unconfirmed.saturating_add(n);
1730 }
1731 let unit = self.q.pop_front().expect("front was just observed");
1732 self.credit(unit.len());
1733 self.starved_from = self.starved_from.saturating_sub(1).max(1);
1734 // Charged to its class like any other release: a flush that
1735 // `write_all` accepted is the only definition of *released
1736 // to the destination stream* this side of the transport,
1737 // and what it could not vouch for is separately reported as
1738 // `QueuedBytesAtTeardown`.
1739 self.note_delivered(&unit);
1740 self.arm_head();
1741 self.sync_shape_demand();
1742 }
1743 Err(_) => return DrainOutcome::WriteFailed,
1744 }
1745 }
1746 DrainOutcome::Complete
1747 }
1748}
1749
1750/// Withdraw whatever demand this queue still holds.
1751///
1752/// `Drop` rather than a call on each teardown path, and for the same reason
1753/// `StreamGuard` is: a forwarding task can end in at least five different
1754/// ways, any hand-written list of them is only as complete as its reader,
1755/// and `Drop` additionally covers a `?` return, a panicking task, and
1756/// `JoinSet::shutdown` dropping the future wholesale. A leaked declaration
1757/// is not a loud failure — it is a *different* stream stalling to
1758/// `max_hold`, on a class the reader was not looking at.
1759impl Drop for PendingQueue {
1760 fn drop(&mut self) {
1761 if let (Some(shaper), Some(class)) = (&self.shaper, self.shape_demand.take()) {
1762 shaper.withdraw_demand(class);
1763 }
1764 // The session gauge counts bytes that are still *somewhere*, and a
1765 // queue going away takes its remainder with it. Without this a
1766 // stream torn down with a full queue would leave the session's
1767 // total permanently above zero, and every later close would spend
1768 // its whole drain window waiting for a queue that no longer exists.
1769 self.credit(self.queued_bytes);
1770 }
1771}
1772
1773/// Write everything queued **at its release time**, racing cancellation.
1774///
1775/// The shape both release-honouring drains take — the FIN drain in the read
1776/// arm's `None` branch and the one a terminal runs before it resets. Both
1777/// sit inside `select!` arm bodies, which are not preemptible, so writing
1778/// either as a plain `while let` loop would let a `Hold` on a gate nobody
1779/// releases pin session teardown for up to [`EgressConfig::max_hold`] —
1780/// precisely the failure the deque was chosen over a writer task to avoid.
1781///
1782/// The inner `select!` is `biased` so cancellation wins deterministically
1783/// when both are ready. Unbiased, a cancelled session could keep picking
1784/// the release branch, find nothing due, and spin.
1785///
1786/// Release lateness is **not** sampled here: this drain honours release
1787/// times only until cancellation, and recording a teardown flush as a
1788/// timing sample would report teardown as a timing failure.
1789///
1790/// # Shaping **is** reported here
1791///
1792/// `on_shape` is called once per [`ShapeReport`] the drain's own
1793/// [`PendingQueue::pop_next_due`] produces, and it exists because this loop
1794/// is a full release seam and not a teardown flush: on a shaped queue every
1795/// pop below debits a token bucket and every clamp or expiry is decided
1796/// here. The FIN path — header, a few objects, FIN, the ordinary MoQT
1797/// subgroup shape — reaches the wire through *this* function and not
1798/// through `release_due_units`, so a drain that swallowed its reports would
1799/// apply the whole profile to the normal case and say nothing about it.
1800/// That is the silent no-op this signature exists to prevent, and the reason
1801/// the parameter is not optional: a caller cannot forget what it has to name.
1802///
1803/// Passing a no-op closure is correct only where no report can exist —
1804/// every `#[cfg(test)]` caller in this module builds an unshaped queue, and
1805/// an unshaped queue's `pop_next_due` never reaches a scheduler.
1806pub(crate) async fn drain_honouring_release_times<S, F>(
1807 pending: &mut PendingQueue,
1808 send: &mut S,
1809 cancel: &CancellationToken,
1810 mut on_shape: F,
1811) -> Result<DrainOutcome, EgressError>
1812where
1813 S: EgressSink,
1814 F: FnMut(ShapeReport),
1815{
1816 while let Some(release) = pending.head_release() {
1817 tokio::select! {
1818 biased;
1819 () = cancel.cancelled() => {
1820 return Ok(match pending.drain_ignoring_release_times(send).await {
1821 DrainOutcome::Terminated { forwarded, code } => {
1822 DrainOutcome::Terminated { forwarded, code }
1823 }
1824 _ => DrainOutcome::CancelledMidDrain,
1825 });
1826 }
1827 () = wait_release(Some(release), cancel) => {
1828 let now = Instant::now();
1829 while let Some(unit) = pending.pop_next_due(now) {
1830 // Before the write, exactly as `release_due_units` does:
1831 // a terminal write returns out of this loop, and an
1832 // expiry's report belongs to the very unit whose write
1833 // takes that return.
1834 if let Some(report) = pending.take_shape_report() {
1835 on_shape(report);
1836 }
1837 if let Written::Terminated { forwarded, code } = write_unit(unit, send).await? {
1838 pending.clear();
1839 return Ok(DrainOutcome::Terminated { forwarded, code });
1840 }
1841 }
1842 // A refusal reports too. The clamp is decided inside
1843 // `shaping_grants_head`, which runs whether or not the head
1844 // is yielded, so reading the report only after a successful
1845 // pop would lose the one case that matters.
1846 if let Some(report) = pending.take_shape_report() {
1847 on_shape(report);
1848 }
1849 }
1850 }
1851 }
1852 Ok(DrainOutcome::Complete)
1853}
1854
1855// ── Session close ───────────────────────────────────────────────────
1856
1857/// The default `(code, reason)` `run_with_transport` closes with when no
1858/// hook asked for anything else — today's hard-coded pair.
1859const DEFAULT_CLOSE: (u32, &[u8]) = (0, b"proxy session ended");
1860
1861/// Where [`crate::action::Action::CloseSession`] lands.
1862///
1863/// A `OnceLock<(u32, Bytes)>` plus the session's `CancellationToken`, and
1864/// deliberately **no `Transport` handles**: the transports own the tasks
1865/// that own the hooks that reach this, so holding them here would close a
1866/// reference cycle. The close itself happens where it already happens, in
1867/// `run_with_transport`, which reads [`Self::close_args`] instead of
1868/// hard-coding `(0, b"proxy session ended")`.
1869///
1870/// `CloseSession` is honoured at **every** site that returns an `Action`,
1871/// including `Site::StreamEnd` on both data and control streams: a close is
1872/// session-scoped, so no site can be the wrong one for it.
1873#[derive(Clone, Debug)]
1874pub(crate) struct SessionCloser {
1875 inner: Arc<CloserInner>,
1876}
1877
1878#[derive(Debug)]
1879struct CloserInner {
1880 request: OnceLock<(u32, Bytes, CloseOrigin)>,
1881 cancel: CancellationToken,
1882}
1883
1884/// Who asked for a session close.
1885///
1886/// Carried alongside the code and the reason because
1887/// [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
1888/// names the cause in prose, and the two callers are not interchangeable to
1889/// anyone reading that: a hook's `Action::CloseSession` is the run
1890/// under test deciding something, while
1891/// [`ProxyControl::close_session`](crate::control::ProxyControl::close_session)
1892/// is the operator outside it pulling the plug. The recorded pair alone
1893/// cannot tell them apart — both arrive as a `u32` and some bytes — and
1894/// before this every control-plane close was reported to observers as a
1895/// hook's, which is a sentence about the run that was simply untrue.
1896#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1897pub(crate) enum CloseOrigin {
1898 /// A hook returned [`crate::action::Action::CloseSession`].
1899 Hook,
1900 /// [`ProxyControl::close_session`](crate::control::ProxyControl::close_session).
1901 ControlPlane,
1902}
1903
1904impl SessionCloser {
1905 /// A closer bound to the session's cancellation token.
1906 pub(crate) fn new(cancel: CancellationToken) -> Self {
1907 Self { inner: Arc::new(CloserInner { request: OnceLock::new(), cancel }) }
1908 }
1909
1910 /// Record a hook's close request. The session keeps running until
1911 /// [`Self::cancel`].
1912 ///
1913 /// The first request wins and returns `true`. A later one changes
1914 /// nothing and returns `false` — refuse it with
1915 /// [`Refusal::SessionAlreadyClosing`](crate::capability::Refusal::SessionAlreadyClosing)
1916 /// rather than letting the second reason overwrite the first.
1917 ///
1918 /// Recording and cancelling are two calls so that `exec::execute` can
1919 /// report the close between them. Cancelling wakes every task in the
1920 /// session, and their teardown is reported from other worker threads: a
1921 /// stream-end decision on each stream the close cuts, a
1922 /// `ControlStreamTruncated`. Cancelling as it records would let those
1923 /// reports reach the observer ahead of the `ActionApplied` that caused
1924 /// them whenever the deciding thread is descheduled in between.
1925 pub(crate) fn request(&self, code: u32, reason: Bytes) -> bool {
1926 self.inner.request.set((code, reason, CloseOrigin::Hook)).is_ok()
1927 }
1928
1929 /// End the session: cancel its token. Idempotent.
1930 pub(crate) fn cancel(&self) {
1931 self.inner.cancel.cancel();
1932 }
1933
1934 /// Record a close request **without** cancelling the session.
1935 ///
1936 /// The first request wins and returns `true`, exactly as
1937 /// [`Self::request`] does; the only difference is the origin it is
1938 /// recorded under.
1939 ///
1940 /// A control-plane close has two steps — fix the code and reason, then
1941 /// give the egress queues a bounded window to flush — so its caller
1942 /// cancels only after the drain. A cancel at this point would end the
1943 /// session before the drain it just asked for had begun, and the drain
1944 /// window would be unobservable.
1945 ///
1946 /// Nothing else changes: `run_with_transport` still reads
1947 /// [`Self::close_args`] at teardown, so a session recorded this way
1948 /// closes with the recorded pair whenever it does close — whether that
1949 /// is the caller cancelling after its drain, or the peer going away
1950 /// first.
1951 pub(crate) fn record(&self, code: u32, reason: Bytes) -> bool {
1952 self.inner.request.set((code, reason, CloseOrigin::ControlPlane)).is_ok()
1953 }
1954
1955 /// Whether a close has been requested.
1956 ///
1957 /// `session.rs` reads [`Self::requested`] instead, because it wants the
1958 /// code and the reason as well; this stays as the cheap yes-or-no
1959 /// predicate for callers that need nothing more.
1960 #[allow(dead_code)]
1961 pub(crate) fn is_closing(&self) -> bool {
1962 self.inner.request.get().is_some()
1963 }
1964
1965 /// What was requested and by whom, if anything.
1966 ///
1967 /// The origin rides along rather than being a second accessor because
1968 /// the two are set together, under one `OnceLock`, and a caller that
1969 /// read them in two calls would be writing a race it does not have.
1970 pub(crate) fn requested(&self) -> Option<(u32, Bytes, CloseOrigin)> {
1971 self.inner.request.get().cloned()
1972 }
1973
1974 /// The `(code, reason)` to hand `Transport::close`, falling back to the
1975 /// pair the proxy has always used.
1976 /// The origin is deliberately dropped here: it is a fact about this
1977 /// process, and what goes on the wire is what the peer was told. A
1978 /// `CONNECTION_CLOSE` carrying *the control plane asked* would be this
1979 /// proxy leaking its own topology into somebody else's session.
1980 pub(crate) fn close_args(&self) -> (u32, Bytes) {
1981 self.inner
1982 .request
1983 .get()
1984 .map(|(code, reason, _)| (*code, reason.clone()))
1985 .unwrap_or_else(|| (DEFAULT_CLOSE.0, Bytes::from_static(DEFAULT_CLOSE.1)))
1986 }
1987}
1988
1989#[cfg(test)]
1990mod tests {
1991 use super::*;
1992
1993 /// An [`EgressSink`] that records instead of sending.
1994 #[derive(Debug, Default)]
1995 struct RecordingSink {
1996 writes: Vec<Bytes>,
1997 reset_code: Option<u64>,
1998 /// Fail every write once this many have succeeded.
1999 fail_after: Option<usize>,
2000 }
2001
2002 impl RecordingSink {
2003 fn written(&self) -> Bytes {
2004 let mut out = Vec::new();
2005 for w in &self.writes {
2006 out.extend_from_slice(w);
2007 }
2008 Bytes::from(out)
2009 }
2010 }
2011
2012 impl EgressSink for RecordingSink {
2013 fn write_all(
2014 &mut self,
2015 buf: &[u8],
2016 ) -> impl Future<Output = Result<(), EgressError>> + Send {
2017 let out = if self.fail_after.is_some_and(|n| self.writes.len() >= n) {
2018 Err(EgressError::Transport(TransportError::Write(
2019 "recording sink is full".to_owned(),
2020 )))
2021 } else {
2022 self.writes.push(Bytes::copy_from_slice(buf));
2023 Ok(())
2024 };
2025 async move { out }
2026 }
2027
2028 fn reset(&mut self, code: u64) -> Result<(), EgressError> {
2029 self.reset_code = Some(code);
2030 Ok(())
2031 }
2032 }
2033
2034 /// The `on_shape` every drain in this module passes.
2035 ///
2036 /// Sound only because every queue built here is **unshaped** — `queue()`
2037 /// and `queue_with()` call `PendingQueue::new` and never
2038 /// `with_shaper`, and `pop_next_due` reaches no scheduler without one —
2039 /// so no `ShapeReport` can exist to swallow. A `unreachable!` rather than
2040 /// an empty body, so a later edit that installs a shaper here finds out
2041 /// instead of inheriting that silent no-op a second time.
2042 fn no_shape_reports(report: ShapeReport) {
2043 unreachable!("an unshaped queue produces no shaping report, got {report:?}");
2044 }
2045
2046 fn queue() -> (PendingQueue, Arc<Recorder>) {
2047 let counters = Arc::new(Recorder::new());
2048 (PendingQueue::new(EgressConfig::default(), Arc::clone(&counters)), counters)
2049 }
2050
2051 fn queue_with(config: EgressConfig) -> (PendingQueue, Arc<Recorder>) {
2052 let counters = Arc::new(Recorder::new());
2053 (PendingQueue::new(config, Arc::clone(&counters)), counters)
2054 }
2055
2056 // ── shaped queues ───────────────────────────────────────────────
2057 //
2058 // Every queue above is unshaped, which is deliberate: the ~25 tests in
2059 // this module encode invariants that must hold whether or not a profile
2060 // is configured, and a shaper installed on all of them would stop being
2061 // separable from them. The handful below install one.
2062
2063 use crate::shape::{
2064 BucketConfig, ClassRule, DirectionStats, Discipline, Expiry, Matcher, Overflow,
2065 QueueConfig, ShapeProfile, ShapeRecorder,
2066 };
2067
2068 /// One named bucket. Struct literals rather than field assignment:
2069 /// `#[non_exhaustive]` does not apply inside the defining crate, and
2070 /// `..Default::default()` is what clippy's `field_reassign_with_default`
2071 /// asks for.
2072 fn test_bucket(rate: Option<u64>, burst: u64) -> BucketConfig {
2073 BucketConfig {
2074 name: "b".to_string(),
2075 rate_bps: rate,
2076 burst_bytes: burst,
2077 ..BucketConfig::default()
2078 }
2079 }
2080
2081 /// One class over bucket `b`, claiming every unit.
2082 fn test_class(name: &str, priority: u8) -> ClassRule {
2083 ClassRule {
2084 name: name.to_string(),
2085 bucket: "b".to_string(),
2086 matcher: Matcher::default(),
2087 priority,
2088 weight: 1,
2089 }
2090 }
2091
2092 /// A one-class profile over one bucket, with everything a shaped queue
2093 /// reads spelled out.
2094 fn profile(rate: Option<u64>, burst: u64, max_hold: Duration, expiry: Expiry) -> ShapeProfile {
2095 let queue = QueueConfig {
2096 max_hold: Some(max_hold),
2097 overflow: Overflow::Block,
2098 on_expiry: expiry,
2099 ..QueueConfig::default()
2100 };
2101 ShapeProfile::try_new(
2102 vec![test_bucket(rate, burst)],
2103 vec![test_class("only", 0)],
2104 queue,
2105 Discipline::Fifo,
2106 )
2107 .expect("the fixture names its own bucket")
2108 }
2109
2110 /// A queue paced by `profile`, plus the recorder it writes to.
2111 ///
2112 /// One leg, because one leg is all these tests are about; the leg the
2113 /// totals actually land on is
2114 /// [`two_legs_sharing_one_recorder_charge_their_own_side`].
2115 fn shaped(profile: ShapeProfile) -> (PendingQueue, Arc<ShapeRecorder>, Arc<Scheduler>) {
2116 let counters = Arc::new(Recorder::new());
2117 let stats = Arc::new(ShapeRecorder::for_profile(Some(&profile)));
2118 let shaper = Arc::new(Scheduler::new(profile));
2119 let mut q = PendingQueue::new(EgressConfig::default(), counters).with_shaper(
2120 Some(Arc::clone(&shaper)),
2121 Arc::clone(&stats),
2122 ProxySide::ClientToProxy,
2123 );
2124 q.tag_unit(Class::Rule(0));
2125 (q, stats, shaper)
2126 }
2127
2128 fn payload(n: usize) -> Bytes {
2129 Bytes::from(vec![0xAB; n])
2130 }
2131
2132 /// The seam. A shaped head that is due on its own account is still held
2133 /// back by its bucket, and the deadline `head_release` then reports is
2134 /// the one the **scheduler** computed — not the head's own `due_at`,
2135 /// which is already in the past.
2136 ///
2137 /// That last clause is the whole reason `arm_head_at` exists: re-arming
2138 /// at `due_at` would hand back an already-fired deadline and spin the
2139 /// release branch hot until the bucket refilled.
2140 #[test]
2141 fn a_shaped_head_waits_for_its_bucket_and_reports_the_scheduler_s_deadline() {
2142 // 1000 bytes/s, 1000-byte burst: the first 1000-byte unit fits and
2143 // the second needs a full second.
2144 let (mut q, _stats, _s) =
2145 shaped(profile(Some(1_000), 1_000, Duration::from_secs(60), Expiry::Deliver));
2146 let now = Instant::now();
2147 q.push(Pending::bytes(payload(1_000), now));
2148 q.push(Pending::bytes(payload(1_000), now));
2149
2150 assert!(q.pop_next_due(now).is_some(), "the burst covers the first unit");
2151 assert!(
2152 q.pop_next_due(now).is_none(),
2153 "the burst is spent, so the second unit waits on a refill"
2154 );
2155 // The head is due on its own account — it was pushed at `now` — so a
2156 // deadline that had not been re-armed would already have fired.
2157 let release = q.head_release().expect("a non-empty queue always has one");
2158 assert!(
2159 !release.deadline().token().is_cancelled(),
2160 "the head must be armed at the refill instant, not at its own past due_at"
2161 );
2162 assert!(
2163 q.pop_next_due(now + Duration::from_secs(1)).is_some(),
2164 "a second's refill covers it"
2165 );
2166 assert!(q.is_empty());
2167 }
2168
2169 /// The spin guard.
2170 ///
2171 /// A `Hold` whose gate has been released makes its unit due, and the gate
2172 /// stays released forever. If the queue kept reporting that gate while
2173 /// the shaper was refusing the same unit, `wait_release` would resolve on
2174 /// every `select!` iteration and the pipe loop would spin hot for the
2175 /// whole of `max_hold`.
2176 #[test]
2177 fn a_shaper_holding_a_gated_unit_does_not_report_the_gate_that_freed_it() {
2178 let (mut q, _stats, _s) =
2179 shaped(profile(Some(0), 0, Duration::from_secs(60), Expiry::Deliver));
2180 let now = Instant::now();
2181 let gate = Gate::new();
2182 // A `Hold`'s shape: due at the ceiling, released early by its gate.
2183 q.push(Pending::bytes(payload(16), now + Duration::from_secs(30)).with_gate(gate.clone()));
2184
2185 assert!(q.pop_next_due(now).is_none(), "an unreleased gate leaves it not due");
2186 assert!(
2187 q.head_release().expect("queued").gate.is_some_and(|g| !g.is_released()),
2188 "before the shaper has an opinion, the unit's own gate is what it waits on"
2189 );
2190
2191 gate.release();
2192 assert!(q.pop_next_due(now).is_none(), "released and due, but the bucket refuses");
2193 let release = q.head_release().expect("still queued");
2194 assert!(
2195 !release.gate.is_some_and(|g| g.is_released()),
2196 "a released Hold gate must not be re-reported while the shaper holds the unit: \
2197 wait_release would resolve on every iteration and spin the pipe loop"
2198 );
2199 }
2200
2201 /// An **unshaped** queue is untouched by any of this: no scheduler, no
2202 /// tag, no clock read beyond what it already did.
2203 ///
2204 /// The control for every test above it. Without this row a bug that
2205 /// paced everything unconditionally would still leave the shaped tests
2206 /// green.
2207 #[test]
2208 fn an_unshaped_queue_pops_a_due_head_with_no_shaper_at_all() {
2209 let (mut q, _c) = queue();
2210 assert!(!q.is_shaped());
2211 let now = Instant::now();
2212 q.push(Pending::bytes(payload(1_000_000), now));
2213 assert!(q.pop_next_due(now).is_some(), "an unshaped queue has nothing to ask");
2214 }
2215
2216 /// A queued terminal bypasses the pacer, on a bucket that grants nothing
2217 /// at all.
2218 ///
2219 /// This is the teardown-bypasses-the-pacer rule at the one place it is
2220 /// not obvious: a `Truncate` or a hook's `ResetStream` is a *queued*
2221 /// unit, so it reaches `pop_next_due` like any other. Gating it would
2222 /// make the eleven data-then-teardown ordering tests depend on a token
2223 /// bucket.
2224 ///
2225 /// # Why the `Truncate` arm is the one that matters
2226 ///
2227 /// A bare `Terminal::Reset` carries **zero bytes**, and a zero-byte
2228 /// charge is granted by any bucket — `tokens >= 0` holds even at rate
2229 /// zero — so a `Reset`-only fixture passes with the guard deleted and
2230 /// proves nothing. A `Truncate` owes its prefix to the wire, and that
2231 /// prefix is exactly what a dry bucket would sit on. Both are asserted,
2232 /// in that order, so the row says which of the two the guard is for.
2233 #[test]
2234 fn a_terminal_is_never_gated_by_a_bucket() {
2235 let (mut q, _stats, _s) =
2236 shaped(profile(Some(0), 0, Duration::from_secs(60), Expiry::Deliver));
2237
2238 q.push(Pending::terminal(Terminal::Reset { code: 7 }));
2239 // `Pending::terminal` stamps its own `due_at` from the clock, so the
2240 // pop instant has to be taken after the push or the unit is simply
2241 // not due yet and this would pass without asking the shaper anything.
2242 let unit = q.pop_next_due(Instant::now()).expect("a reset consults no bucket");
2243 assert!(unit.is_terminal());
2244
2245 q.push(Pending::terminal(Terminal::Truncate { prefix: payload(64), code: 9 }));
2246 let unit = q
2247 .pop_next_due(Instant::now())
2248 .expect("a truncate owes its prefix to the wire, not to a bucket");
2249 assert_eq!(unit.len(), 64, "and the prefix goes with it");
2250 }
2251
2252 /// Under the default `Expiry::Deliver` a class that can never be
2253 /// granted from tokens **still delivers**, at `max_hold`, and says so.
2254 ///
2255 /// The two halves are one test because the report is what keeps the
2256 /// clamp from being a silent lateness, and the delivery is what keeps a
2257 /// starved class from being a silent loss.
2258 ///
2259 /// The report is asserted as a **whole variant**, `requested` included,
2260 /// and not as `matches!(.., Clamped { applied, .. })`: a zero-rate
2261 /// bucket names no refill instant, so the request this clamp cut short
2262 /// is unbounded and the only honest value for it is absent. Written as
2263 /// a wildcard it would pass against a `Duration::MAX` sentinel, which
2264 /// `an_unbounded_shaping_wait_reports_no_request_at_all` exists to rule
2265 /// out.
2266 #[test]
2267 fn a_zero_rate_class_still_delivers_at_the_clamp_and_reports_it() {
2268 const HOLD: Duration = Duration::from_millis(80);
2269 let (mut q, stats, _s) = shaped(profile(Some(0), 0, HOLD, Expiry::Deliver));
2270 let now = Instant::now();
2271 q.push(Pending::bytes(payload(16), now));
2272
2273 assert!(q.pop_next_due(now).is_none(), "a zero-rate bucket grants nothing");
2274 assert_eq!(
2275 stats.snapshot().classes[0].tokens_exhausted_episodes,
2276 1,
2277 "and says its own bucket was dry, once per episode"
2278 );
2279 assert_eq!(q.take_shape_report(), None, "nothing is clamped before the clamp");
2280
2281 let unit = q
2282 .pop_next_due(now + HOLD + Duration::from_millis(1))
2283 .expect("Expiry::Deliver clamps and delivers");
2284 assert_eq!(unit.len(), 16);
2285 assert_eq!(
2286 q.take_shape_report(),
2287 Some(ShapeReport::Clamped { requested: None, applied: HOLD }),
2288 "a clamped release reports the clamp it applied, and reports the \
2289 request it cut short as absent because a zero-rate bucket named none"
2290 );
2291 assert_eq!(stats.snapshot().classes[0].bytes_delivered, 16);
2292 assert_eq!(stats.snapshot().objects_expired, 0, "Deliver never expires an object");
2293 }
2294
2295 /// A shaping clamp reports **no request at all**, on both of the two
2296 /// ways a bucket can decline to name a refill instant.
2297 /// The two legs are the two refusals that name no instant: a rate of zero,
2298 /// and a unit larger than the burst the bucket can ever hold. Both are
2299 /// released by the clamp and by nothing else, and neither has a duration to
2300 /// quote — the wait was unbounded. Leg (b) additionally has a **positive**
2301 /// rate, so it rules out *the report is absent because the class is
2302 /// switched off*.
2303 ///
2304 /// Asserted as a whole-variant equality, which is the point: a
2305 /// `Duration::MAX` sentinel here would satisfy every inequality an author
2306 /// would naturally write against the field — `requested > applied` —
2307 /// exactly as a real request does.
2308 /// The **pre-clamp** report is asserted per leg for the same reason, and it
2309 /// is where the two legs stop looking alike: a zero-rate class is silent
2310 /// until its clamp fires, and a class whose burst cannot cover one of its
2311 /// own units says so at the first refusal. That asymmetry is the only thing
2312 /// separating *this rate is being applied and it is low* from *this rate is
2313 /// not being applied at all*, since both produce this same `Clamped`
2314 /// afterwards.
2315 #[test]
2316 fn an_unbounded_shaping_wait_reports_no_request_at_all() {
2317 const HOLD: Duration = Duration::from_millis(80);
2318 /// Bigger than the burst leg (b) configures, so the bucket can never
2319 /// hold enough for it however long it accrues.
2320 const OVERSIZED: usize = 4_096;
2321
2322 let legs = [
2323 ("a zero rate", profile(Some(0), 0, HOLD, Expiry::Deliver), 16, None),
2324 (
2325 "a unit above burst_bytes",
2326 profile(Some(64_000), 64, HOLD, Expiry::Deliver),
2327 OVERSIZED,
2328 Some(ShapeReport::BurstBelowUnit {
2329 class: Class::Rule(0),
2330 burst_bytes: 64,
2331 unit_bytes: OVERSIZED as u64,
2332 }),
2333 ),
2334 ];
2335
2336 for (label, profile, bytes, pre_clamp) in legs {
2337 let (mut q, _stats, _s) = shaped(profile);
2338 let now = Instant::now();
2339 q.push(Pending::bytes(payload(bytes), now));
2340
2341 assert!(q.pop_next_due(now).is_none(), "{label}: nothing may be granted up front");
2342 assert_eq!(
2343 q.take_shape_report(),
2344 pre_clamp,
2345 "{label}: nothing is *clamped* before the clamp, and only the \
2346 mis-sized burst says anything at all before it"
2347 );
2348
2349 let unit = q
2350 .pop_next_due(now + HOLD + Duration::from_millis(1))
2351 .unwrap_or_else(|| panic!("{label}: Expiry::Deliver clamps and delivers"));
2352 assert_eq!(unit.len(), bytes, "{label}: the whole unit goes out");
2353 assert_eq!(
2354 q.take_shape_report(),
2355 Some(ShapeReport::Clamped { requested: None, applied: HOLD }),
2356 "{label}: the bucket named no instant, so there is no request to \
2357 quote and the report must say so rather than name a sentinel"
2358 );
2359 }
2360 }
2361
2362 /// `Expiry::ResetStream` abandons the whole destination stream: the
2363 /// queue is replaced by the reset, the object is counted as expired and
2364 /// the stream as reset by shaping.
2365 ///
2366 /// Everything queued goes with it, and that is deliberate — writing part
2367 /// of a stream the shaper has given up on leaves the peer a prefix it
2368 /// cannot tell from a truncation.
2369 #[test]
2370 fn expiry_reset_stream_replaces_the_queue_with_its_reset() {
2371 const HOLD: Duration = Duration::from_millis(50);
2372 let (mut q, stats, _s) =
2373 shaped(profile(Some(0), 0, HOLD, Expiry::ResetStream { code: 0x2A }));
2374 let now = Instant::now();
2375 q.push(Pending::bytes(payload(16), now));
2376 q.push(Pending::bytes(payload(16), now));
2377
2378 let unit = q
2379 .pop_next_due(now + HOLD + Duration::from_millis(1))
2380 .expect("an expired head under ResetStream yields the reset");
2381 assert_eq!(unit.item(), &Item::Terminal(Terminal::Reset { code: 0x2A }));
2382 assert_eq!(q.take_shape_report(), Some(ShapeReport::Expired));
2383 assert!(q.is_empty(), "everything behind it went with the stream");
2384
2385 let snap = stats.snapshot();
2386 assert_eq!(snap.objects_expired, 1);
2387 assert_eq!(snap.streams_reset_by_shaping, 1);
2388 }
2389
2390 /// Two queues, one recorder, one leg each: what a queue charges lands on
2391 /// the leg it was built for.
2392 ///
2393 /// This is the seam between the split totals and the thing that knows
2394 /// which side a stream is on. The recorder is session-scoped and shared
2395 /// by every forwarding task, so it cannot work the direction out for
2396 /// itself; the queue is per stream direction and can. A recorder that
2397 /// could split but that nothing ever told which leg to charge would
2398 /// report one side doing all the work and the other doing none, which
2399 /// is indistinguishable from a session that really was one-way.
2400 ///
2401 /// The two legs are deliberately asymmetric in **opposite** senses —
2402 /// the uplink carries fewer bytes and takes the expiry, the downlink
2403 /// carries six times the bytes and expires nothing — so swapping the
2404 /// rows fails on every field rather than cancelling out.
2405 ///
2406 /// `objects_seen` is zero on both legs, and it is asserted rather than
2407 /// elided: it is charged where a unit is classified, which is the pipe
2408 /// loop's job and not this queue's, so a queue that started moving it
2409 /// would be counting the same object twice.
2410 #[test]
2411 fn two_legs_sharing_one_recorder_charge_their_own_side() {
2412 const HOLD: Duration = Duration::from_millis(50);
2413 // A bucket that grants nothing, so nothing leaves except through
2414 // the clamp, and the clamp is what the expiry arm decides.
2415 let p = profile(Some(0), 0, HOLD, Expiry::ResetStream { code: 0x2A });
2416 let counters = Arc::new(Recorder::new());
2417 let stats = Arc::new(ShapeRecorder::for_profile(Some(&p)));
2418 let shaper = Arc::new(Scheduler::new(p));
2419
2420 let mut up = PendingQueue::new(EgressConfig::default(), Arc::clone(&counters)).with_shaper(
2421 Some(Arc::clone(&shaper)),
2422 Arc::clone(&stats),
2423 ProxySide::ClientToProxy,
2424 );
2425 let mut down = PendingQueue::new(EgressConfig::default(), counters).with_shaper(
2426 Some(shaper),
2427 Arc::clone(&stats),
2428 ProxySide::RelayToProxy,
2429 );
2430
2431 let now = Instant::now();
2432 // Untagged, so these are the units no rule could see — the ones
2433 // charged to `bytes_shaped` as they are queued. Different sizes on
2434 // the two legs, so a leg that counted pushes rather than bytes is
2435 // separable from one that adds.
2436 up.push(Pending::bytes(payload(16), now));
2437 down.push(Pending::bytes(payload(48), now));
2438 down.push(Pending::bytes(payload(48), now));
2439
2440 // The uplink head outlives its clamp and the stream is abandoned.
2441 assert!(
2442 up.pop_next_due(now + HOLD + Duration::from_millis(1)).is_some(),
2443 "an expired head under ResetStream yields the reset"
2444 );
2445 // The downlink's head is inside its clamp and goes out normally —
2446 // unshapeable bytes charge no bucket — so nothing on that leg
2447 // expires and nothing on it is reset.
2448 assert!(down.pop_next_due(now).is_some(), "the downlink head is not past its clamp");
2449
2450 let snap = stats.snapshot();
2451 assert_eq!(
2452 snap.uplink,
2453 DirectionStats {
2454 objects_seen: 0,
2455 bytes_shaped: 16,
2456 objects_expired: 1,
2457 streams_reset_by_shaping: 1,
2458 streams_with_mixed_classes: 0,
2459 },
2460 "the uplink queue's bytes and its expiry are the uplink's"
2461 );
2462 assert_eq!(
2463 snap.downlink,
2464 DirectionStats {
2465 objects_seen: 0,
2466 bytes_shaped: 96,
2467 objects_expired: 0,
2468 streams_reset_by_shaping: 0,
2469 streams_with_mixed_classes: 0,
2470 },
2471 "the downlink queued more and gave nothing up: a stall on one leg \
2472 must not be reported on the other"
2473 );
2474 assert_eq!(snap.bytes_shaped, 112, "the aggregate is the two legs and nothing else");
2475 assert_eq!(snap.objects_expired, 1);
2476 assert_eq!(snap.streams_reset_by_shaping, 1);
2477 }
2478
2479 /// A unit queued behind a unit of a **different** class is counted once,
2480 /// against its own class, and never against `tokens_exhausted_episodes`
2481 /// — which belongs to the class whose bucket was actually dry.
2482 ///
2483 /// Two causes of waiting, two counters. Conflating them is what makes
2484 /// head-of-line blocking read as configured shaping.
2485 #[test]
2486 fn a_unit_behind_another_class_is_counted_once_and_separately() {
2487 // Two classes, one bucket that grants nothing, so the head parks.
2488 let queue =
2489 QueueConfig { max_hold: Some(Duration::from_secs(60)), ..QueueConfig::default() };
2490 let p = ShapeProfile::try_new(
2491 vec![test_bucket(Some(0), 0)],
2492 vec![test_class("head", 0), test_class("behind", 0)],
2493 queue,
2494 Discipline::Fifo,
2495 )
2496 .expect("two uniquely named classes over one bucket");
2497
2498 let counters = Arc::new(Recorder::new());
2499 let stats = Arc::new(ShapeRecorder::for_profile(Some(&p)));
2500 let mut q = PendingQueue::new(EgressConfig::default(), counters).with_shaper(
2501 Some(Arc::new(Scheduler::new(p))),
2502 Arc::clone(&stats),
2503 ProxySide::ClientToProxy,
2504 );
2505
2506 let now = Instant::now();
2507 q.tag_unit(Class::Rule(0));
2508 q.push(Pending::bytes(payload(16), now));
2509 q.tag_unit(Class::Rule(1));
2510 q.push(Pending::bytes(payload(16), now));
2511 q.push(Pending::bytes(payload(16), now));
2512
2513 // Three refusals; the sweep must still charge each waiting unit once.
2514 for _ in 0..3 {
2515 assert!(q.pop_next_due(now).is_none());
2516 }
2517 let snap = stats.snapshot();
2518 assert_eq!(
2519 snap.classes[1].starved_behind_other_class, 2,
2520 "both units behind the other class's head are counted, once each"
2521 );
2522 assert_eq!(
2523 snap.classes[0].starved_behind_other_class, 0,
2524 "the head is not waiting behind anybody"
2525 );
2526 assert_eq!(
2527 snap.classes[0].tokens_exhausted_episodes, 1,
2528 "the dry bucket is the head's own, and it is one episode"
2529 );
2530 }
2531
2532 /// The demand a queue declares is withdrawn when it is dropped.
2533 ///
2534 /// Not a nicety: a declaration that outlives its stream starves every
2535 /// lower-priority class for the rest of the session, and the symptom is
2536 /// a *different* stream stalling to `max_hold`.
2537 #[test]
2538 fn dropping_a_queue_withdraws_the_demand_it_declared() {
2539 let queue =
2540 QueueConfig { max_hold: Some(Duration::from_secs(60)), ..QueueConfig::default() };
2541 let p = ShapeProfile::try_new(
2542 vec![test_bucket(None, 0)],
2543 vec![test_class("hi", 9), test_class("lo", 0)],
2544 queue,
2545 Discipline::StrictPriority,
2546 )
2547 .expect("two uniquely named classes over one bucket");
2548
2549 let counters = Arc::new(Recorder::new());
2550 let stats = Arc::new(ShapeRecorder::for_profile(Some(&p)));
2551 let shaper = Arc::new(Scheduler::new(p));
2552
2553 let now = Instant::now();
2554 {
2555 let mut hi = PendingQueue::new(EgressConfig::default(), Arc::clone(&counters))
2556 .with_shaper(
2557 Some(Arc::clone(&shaper)),
2558 Arc::clone(&stats),
2559 ProxySide::ClientToProxy,
2560 );
2561 hi.tag_unit(Class::Rule(0));
2562 // Due a minute out, so it stays queued and keeps declaring.
2563 hi.push(Pending::bytes(payload(16), now + Duration::from_secs(60)));
2564 assert!(
2565 matches!(shaper.acquire(Class::Rule(1), 16, now), Acquire::Starved(_)),
2566 "the high class is holding the bucket while its queue is alive"
2567 );
2568 }
2569 assert!(
2570 matches!(shaper.acquire(Class::Rule(1), 16, now), Acquire::Now),
2571 "the high class's stream is gone, so nothing is holding the low one back"
2572 );
2573 }
2574
2575 #[test]
2576 fn an_empty_queue_waits_for_nothing_and_accepts_more() {
2577 let (q, _c) = queue();
2578 assert!(q.is_empty());
2579 assert_eq!(q.len(), 0);
2580 assert_eq!(q.queued_bytes(), 0);
2581 assert!(q.accepts_more());
2582 assert!(q.head_release().is_none());
2583 }
2584
2585 #[test]
2586 fn a_later_unit_cannot_overtake_an_earlier_one() {
2587 let (mut q, _c) = queue();
2588 let now = Instant::now();
2589 let head =
2590 q.push(Pending::bytes(Bytes::from_static(b"0"), now + Duration::from_millis(300)));
2591 // A `Pass` — no delay at all — pushed behind it.
2592 let behind = q.push(Pending::bytes(Bytes::from_static(b"1"), now));
2593 assert_eq!(
2594 behind.release_at, head.release_at,
2595 "the reported release is the queue's estimate, and it accounts for the head",
2596 );
2597 // And a third with a smaller delay than the head's.
2598 let third =
2599 q.push(Pending::bytes(Bytes::from_static(b"2"), now + Duration::from_millis(10)));
2600 assert_eq!(third.release_at, head.release_at);
2601 assert_eq!(q.len(), 3);
2602 assert_eq!(q.queued_bytes(), 3);
2603 }
2604
2605 /// The estimate is reporting; `due_at` is readiness. Conflating the two
2606 /// would strand whole streams behind a released `Hold`.
2607 #[test]
2608 fn the_queues_estimate_never_rewrites_a_units_own_deadline() {
2609 let (mut q, _c) = queue();
2610 let now = Instant::now();
2611 q.push(Pending::bytes(Bytes::from_static(b"0"), now + Duration::from_millis(300)));
2612 q.push(Pending::bytes(Bytes::from_static(b"1"), now));
2613 q.push(Pending::bytes(Bytes::from_static(b"2"), now + Duration::from_millis(10)));
2614
2615 // Popped at a time past everything, so the deque hands all three
2616 // back and each can be asked what it was actually built with.
2617 let far = now + Duration::from_secs(1);
2618 let units: Vec<Pending> = std::iter::from_fn(|| q.pop_next_due(far)).collect();
2619 assert_eq!(units.len(), 3);
2620 assert_eq!(units[0].due_at(), now + Duration::from_millis(300));
2621 assert_eq!(units[1].due_at(), now, "a `Pass` behind a delay keeps its own `now`");
2622 assert_eq!(units[2].due_at(), now + Duration::from_millis(10));
2623 // The estimate is the clamped one, on every unit.
2624 for unit in &units {
2625 assert_eq!(unit.expected_at(), units[0].due_at());
2626 }
2627 }
2628
2629 #[test]
2630 fn pushing_bumps_the_egress_counter_once_per_unit() {
2631 let (mut q, counters) = queue();
2632 let now = Instant::now();
2633 for _ in 0..4 {
2634 q.push(Pending::bytes(Bytes::from_static(b"x"), now));
2635 }
2636 assert_eq!(counters.snapshot().egress_items_queued, 4);
2637 }
2638
2639 #[test]
2640 fn a_unit_that_is_due_pops_and_one_that_is_not_does_not() {
2641 let (mut q, _c) = queue();
2642 let now = Instant::now();
2643 q.push(Pending::bytes(Bytes::from_static(b"soon"), now));
2644 q.push(Pending::bytes(Bytes::from_static(b"later"), now + Duration::from_secs(60)));
2645 let first = q.pop_next_due(now).expect("head is due");
2646 assert_eq!(first.len(), 4);
2647 assert!(q.pop_next_due(now).is_none(), "the tail is a minute out");
2648 assert_eq!(q.queued_bytes(), 5);
2649 assert!(q.head_release().is_some());
2650 }
2651
2652 #[test]
2653 fn a_released_gate_makes_a_unit_due_before_its_ceiling() {
2654 let (mut q, _c) = queue();
2655 let now = Instant::now();
2656 let gate = Gate::new();
2657 // The `max_hold` ceiling: 30 s out.
2658 q.push(
2659 Pending::bytes(Bytes::from_static(b"held"), hold_ceiling(now, q.config()))
2660 .with_gate(gate.clone()),
2661 );
2662 assert!(q.pop_next_due(Instant::now()).is_none());
2663 gate.release();
2664 let unit = q
2665 .pop_next_due(Instant::now())
2666 .expect("a released gate is due, or the release arm spins until max_hold");
2667 assert_eq!(unit.len(), 4);
2668 }
2669
2670 /// Releasing a gate frees the held unit **and the run behind it**.
2671 ///
2672 /// The head carries the gate; the three units behind it carry none, so
2673 /// the only thing that can make them due is their own deadline. They
2674 /// have one — `now` — and it is theirs, which is the whole point. When
2675 /// the queue stamped them with the hold's `max_hold` ceiling instead,
2676 /// one gate release wrote one object and stranded every object behind
2677 /// it, plus the stream's FIN, for 30 s.
2678 #[test]
2679 fn releasing_a_gate_frees_the_whole_run_queued_behind_it() {
2680 let (mut q, _c) = queue();
2681 let now = Instant::now();
2682 let gate = Gate::new();
2683 q.push(
2684 Pending::bytes(Bytes::from_static(b"held"), hold_ceiling(now, q.config()))
2685 .with_gate(gate.clone()),
2686 );
2687 // Three plain `Pass` units, decided after the hold, so they are
2688 // pushed for ordering and nothing else.
2689 for tail in [&b"1"[..], b"2", b"3"] {
2690 q.push(Pending::bytes(Bytes::copy_from_slice(tail), Instant::now()));
2691 }
2692 assert_eq!(q.len(), 4);
2693 assert!(q.pop_next_due(Instant::now()).is_none(), "the head holds the whole run");
2694
2695 gate.release();
2696 let at = Instant::now();
2697 let drained: Vec<usize> =
2698 std::iter::from_fn(|| q.pop_next_due(at)).map(|u| u.len()).collect();
2699 assert_eq!(
2700 drained,
2701 vec![4, 1, 1, 1],
2702 "one wake on the gate must free the head *and* everything behind it, in order",
2703 );
2704 assert!(q.is_empty(), "nothing may be left for the max_hold ceiling to release");
2705 assert_eq!(q.queued_bytes(), 0);
2706 }
2707
2708 /// A `Delay` queued behind a `Hold` keeps its **own** deadline.
2709 /// `Action::Delay` documents `arrived_at + by` — *a deadline, not a
2710 /// spacing* — and a `Hold` in front of it is not allowed to redefine that
2711 /// as the hold's ceiling. The unit is not due at the gate release, because
2712 /// its own 50 ms have not passed; it *is* due 50 ms later, rather than at
2713 /// the 20 s ceiling.
2714 #[test]
2715 fn a_delay_queued_behind_a_hold_keeps_its_own_deadline() {
2716 const CEILING: Duration = Duration::from_secs(20);
2717 const BY: Duration = Duration::from_millis(50);
2718
2719 let config = EgressConfig { max_hold: CEILING, ..EgressConfig::default() };
2720 let (mut q, _c) = queue_with(config);
2721 let now = Instant::now();
2722 let gate = Gate::new();
2723 q.push(
2724 Pending::bytes(Bytes::from_static(b"held"), hold_ceiling(now, q.config()))
2725 .with_gate(gate.clone()),
2726 );
2727 let queued = q.push(Pending::bytes(Bytes::from_static(b"late"), now + BY));
2728 assert!(
2729 queued.release_at >= now + CEILING,
2730 "the *estimate* stays conservative — the queue cannot know when a gate opens",
2731 );
2732
2733 gate.release();
2734 let head = q.pop_next_due(now + Duration::from_millis(10)).expect("a released gate is due");
2735 assert_eq!(head.len(), 4);
2736 assert!(
2737 q.pop_next_due(now + Duration::from_millis(10)).is_none(),
2738 "the delayed unit's own deadline still governs it: 10 ms is inside its {BY:?}",
2739 );
2740 let second = q
2741 .pop_next_due(now + BY + Duration::from_millis(10))
2742 .expect("arrived_at + by has passed, and that is the whole of the deadline");
2743 assert_eq!(second.len(), 4);
2744 assert_eq!(second.due_at(), now + BY, "its deadline was never rewritten");
2745 }
2746
2747 #[test]
2748 fn the_byte_budget_stops_the_read_branch_and_reports_once() {
2749 // A struct literal is legal here and not in `tests/`: this file is
2750 // inside the defining crate, where `#[non_exhaustive]` does not
2751 // apply.
2752 let config = EgressConfig { max_pending_bytes: 8, ..EgressConfig::default() };
2753 let (mut q, _c) = queue_with(config);
2754 let now = Instant::now();
2755 let first = q.push(Pending::bytes(Bytes::from_static(b"1234"), now));
2756 assert!(!first.entered_backpressure);
2757 assert!(q.accepts_more());
2758 let second = q.push(Pending::bytes(Bytes::from_static(b"5678"), now));
2759 assert!(second.entered_backpressure, "the transition into backpressure is reported");
2760 assert!(!q.accepts_more());
2761 let third = q.push(Pending::bytes(Bytes::from_static(b"9"), now));
2762 assert!(!third.entered_backpressure, "once per stream, not once per push");
2763 }
2764
2765 #[test]
2766 fn the_unit_cap_bounds_a_queue_of_zero_byte_units() {
2767 let (mut q, _c) = queue();
2768 let now = Instant::now();
2769 let mut reports = 0;
2770 for _ in 0..=MAX_PENDING_UNITS {
2771 if q.push(Pending::elided(now)).entered_backpressure {
2772 reports += 1;
2773 }
2774 }
2775 assert_eq!(q.queued_bytes(), 0, "elided units carry no bytes at all");
2776 assert!(!q.accepts_more(), "the byte budget alone would never stop this");
2777 assert_eq!(reports, 1);
2778 }
2779
2780 #[tokio::test]
2781 async fn an_elided_unit_writes_nothing_but_keeps_its_slot() {
2782 let (mut q, _c) = queue();
2783 let now = Instant::now();
2784 q.push(Pending::bytes(Bytes::from_static(b"a"), now));
2785 q.push(Pending::elided(now));
2786 q.push(Pending::bytes(Bytes::from_static(b"c"), now));
2787 let mut sink = RecordingSink::default();
2788 assert_eq!(q.drain_ignoring_release_times(&mut sink).await, DrainOutcome::Complete);
2789 assert_eq!(sink.written(), Bytes::from_static(b"ac"));
2790 assert_eq!(sink.writes.len(), 2);
2791 }
2792
2793 #[tokio::test]
2794 async fn a_truncate_terminal_writes_its_prefix_then_resets() {
2795 let (mut q, _c) = queue();
2796 let now = Instant::now();
2797 q.push(Pending::bytes(Bytes::from_static(b"ahead"), now));
2798 q.push(Pending::terminal(Terminal::Truncate {
2799 prefix: Bytes::from_static(b"pre"),
2800 code: 0x2,
2801 }));
2802 q.push(Pending::bytes(Bytes::from_static(b"never"), now));
2803 let mut sink = RecordingSink::default();
2804 let outcome = q.drain_ignoring_release_times(&mut sink).await;
2805 assert_eq!(outcome, DrainOutcome::Terminated { forwarded: 3, code: 0x2 });
2806 assert_eq!(sink.written(), Bytes::from_static(b"aheadpre"));
2807 assert_eq!(sink.reset_code, Some(0x2));
2808 assert!(q.is_empty(), "nothing behind a terminal is written");
2809 assert_eq!(q.queued_bytes(), 0);
2810 }
2811
2812 #[tokio::test]
2813 async fn a_reset_terminal_writes_nothing() {
2814 let mut sink = RecordingSink::default();
2815 let written = write_unit(Pending::terminal(Terminal::Reset { code: 7 }), &mut sink)
2816 .await
2817 .expect("reset never fails the write");
2818 assert_eq!(written, Written::Terminated { forwarded: 0, code: 7 });
2819 assert!(sink.writes.is_empty());
2820 assert_eq!(sink.reset_code, Some(7));
2821 }
2822
2823 #[tokio::test]
2824 async fn a_failed_drain_leaves_what_it_could_not_write_queued() {
2825 let (mut q, _c) = queue();
2826 let now = Instant::now();
2827 q.push(Pending::bytes(Bytes::from_static(b"aa"), now));
2828 q.push(Pending::bytes(Bytes::from_static(b"bbb"), now));
2829 q.push(Pending::bytes(Bytes::from_static(b"cccc"), now));
2830 let mut sink = RecordingSink { fail_after: Some(1), ..RecordingSink::default() };
2831 assert_eq!(q.drain_ignoring_release_times(&mut sink).await, DrainOutcome::WriteFailed);
2832 assert_eq!(sink.written(), Bytes::from_static(b"aa"));
2833 assert_eq!(q.len(), 2);
2834 // This is what `Impairment { QueuedBytesAtTeardown { bytes } }` carries.
2835 assert_eq!(q.queued_bytes(), 7);
2836 }
2837
2838 #[tokio::test]
2839 async fn the_honouring_drain_writes_in_order_at_release_time() {
2840 let (mut q, counters) = queue();
2841 let now = Instant::now();
2842 let head_due = now + Duration::from_millis(30);
2843 q.push(Pending::bytes(Bytes::from_static(b"0"), head_due));
2844 q.push(Pending::bytes(Bytes::from_static(b"1"), now));
2845 q.push(Pending::bytes(Bytes::from_static(b"2"), now));
2846 let cancel = CancellationToken::new();
2847 let mut sink = RecordingSink::default();
2848 let outcome = drain_honouring_release_times(&mut q, &mut sink, &cancel, no_shape_reports)
2849 .await
2850 .expect("no write failed");
2851 assert_eq!(outcome, DrainOutcome::Complete);
2852 // Measured against the head's own deadline, not a fresh clock read
2853 // taken after the pushes: the drain promises to write nothing before
2854 // `head_due`, and time the test thread spends preempted between
2855 // `now` and a later read is not time the drain owes.
2856 assert!(Instant::now() >= head_due, "release times were honoured");
2857 assert_eq!(sink.written(), Bytes::from_static(b"012"));
2858 assert_eq!(counters.snapshot().release_errors.count, 0, "drains are never release samples");
2859 assert_eq!(
2860 q.unconfirmed_bytes(),
2861 0,
2862 "a drain that ran to completion at its release times is not a teardown and owes \
2863 no `QueuedBytesAtTeardown`",
2864 );
2865 }
2866
2867 /// A teardown flush reports what it handed to a dying transport.
2868 ///
2869 /// The teardown-race loss, at queue level. `drain_ignoring_release_times`
2870 /// writes the held unit and `write_all` returns `Ok` — quinn buffered it —
2871 /// so **nothing is left queued**, and a report built on
2872 /// [`PendingQueue::queued_bytes`] says zero for exactly the case where
2873 /// `run_with_transport`'s `close()` then discards the buffer and the
2874 /// object never reaches the peer. `a_held_object_is_never_silently_lost_at_teardown`
2875 /// is the same claim over real QUIC; this is the mechanism, with no
2876 /// scheduler in it.
2877 #[tokio::test]
2878 async fn a_teardown_flush_reports_what_it_handed_to_a_dying_transport() {
2879 let (mut q, _c) = queue();
2880 // A hold nobody will release, so only the cancel fallback can move
2881 // it.
2882 q.push(
2883 Pending::bytes(Bytes::from_static(b"gone"), hold_ceiling(Instant::now(), q.config()))
2884 .with_gate(Gate::new()),
2885 );
2886 let cancel = CancellationToken::new();
2887 cancel.cancel();
2888 let mut sink = RecordingSink::default();
2889
2890 let outcome = drain_honouring_release_times(&mut q, &mut sink, &cancel, no_shape_reports)
2891 .await
2892 .expect("the fallback drain swallows write failures");
2893 assert_eq!(outcome, DrainOutcome::CancelledMidDrain);
2894 assert_eq!(sink.written(), Bytes::from_static(b"gone"), "delivered late beats lost");
2895 assert_eq!(q.queued_bytes(), 0, "the fallback handed everything to the transport");
2896 assert_eq!(
2897 q.unconfirmed_bytes(),
2898 4,
2899 "…and handing bytes to a transport the session is closing is not delivering \
2900 them: this is what `QueuedBytesAtTeardown` has to carry, or the object is \
2901 gone with no event at all",
2902 );
2903 }
2904
2905 #[tokio::test]
2906 async fn a_teardown_flush_that_could_not_write_reports_both_halves() {
2907 let (mut q, _c) = queue();
2908 let now = Instant::now();
2909 q.push(Pending::bytes(Bytes::from_static(b"aa"), now));
2910 q.push(Pending::bytes(Bytes::from_static(b"bbb"), now));
2911 q.push(Pending::bytes(Bytes::from_static(b"cccc"), now));
2912 let mut sink = RecordingSink { fail_after: Some(1), ..RecordingSink::default() };
2913 assert_eq!(q.drain_ignoring_release_times(&mut sink).await, DrainOutcome::WriteFailed);
2914 assert_eq!(q.queued_bytes(), 7, "what never reached the transport");
2915 assert_eq!(q.unconfirmed_bytes(), 9, "…plus the two bytes that did, and may be lost");
2916 }
2917
2918 #[tokio::test]
2919 async fn cancelling_a_drain_that_is_waiting_on_a_gate_falls_back_at_once() {
2920 let (mut q, _c) = queue();
2921 let now = Instant::now();
2922 // A hold nobody will ever release, at the 30 s ceiling.
2923 q.push(
2924 Pending::bytes(Bytes::from_static(b"held"), hold_ceiling(now, q.config()))
2925 .with_gate(Gate::new()),
2926 );
2927 let cancel = CancellationToken::new();
2928 let waker = cancel.clone();
2929 tokio::spawn(async move {
2930 tokio::time::sleep(Duration::from_millis(20)).await;
2931 waker.cancel();
2932 });
2933 let mut sink = RecordingSink::default();
2934 let started = Instant::now();
2935 let outcome = drain_honouring_release_times(&mut q, &mut sink, &cancel, no_shape_reports)
2936 .await
2937 .expect("no write failed");
2938 assert_eq!(outcome, DrainOutcome::CancelledMidDrain);
2939 assert!(
2940 started.elapsed() < Duration::from_secs(2),
2941 "teardown must not wait out max_hold; took {:?}",
2942 started.elapsed(),
2943 );
2944 assert_eq!(
2945 sink.written(),
2946 Bytes::from_static(b"held"),
2947 "delivered late beats lost silently"
2948 );
2949 assert_eq!(q.queued_bytes(), 0);
2950 }
2951
2952 #[tokio::test]
2953 async fn an_already_cancelled_drain_does_not_spin() {
2954 let (mut q, _c) = queue();
2955 q.push(Pending::bytes(Bytes::from_static(b"x"), Instant::now() + Duration::from_secs(60)));
2956 let cancel = CancellationToken::new();
2957 cancel.cancel();
2958 let mut sink = RecordingSink::default();
2959 let started = Instant::now();
2960 let outcome = drain_honouring_release_times(&mut q, &mut sink, &cancel, no_shape_reports)
2961 .await
2962 .unwrap();
2963 assert_eq!(outcome, DrainOutcome::CancelledMidDrain);
2964 assert!(started.elapsed() < Duration::from_secs(1), "the biased arm must win");
2965 assert_eq!(sink.written(), Bytes::from_static(b"x"));
2966 }
2967
2968 #[tokio::test]
2969 async fn wait_release_resolves_on_the_gate_the_deadline_or_the_cancel() {
2970 let (mut q, _c) = queue();
2971 let far = Instant::now() + Duration::from_secs(60);
2972
2973 // 1. The gate.
2974 let gate = Gate::new();
2975 q.push(Pending::bytes(Bytes::from_static(b"g"), far).with_gate(gate.clone()));
2976 gate.release();
2977 let cancel = CancellationToken::new();
2978 wait_release(q.head_release(), &cancel).await;
2979
2980 // 2. The deadline.
2981 let (mut q, _c) = queue();
2982 q.push(Pending::bytes(
2983 Bytes::from_static(b"d"),
2984 Instant::now() + Duration::from_millis(20),
2985 ));
2986 let started = Instant::now();
2987 wait_release(q.head_release(), &cancel).await;
2988 assert!(started.elapsed() >= Duration::from_millis(20));
2989
2990 // 3. The session cancel.
2991 let (mut q, _c) = queue();
2992 q.push(Pending::bytes(Bytes::from_static(b"c"), far));
2993 cancel.cancel();
2994 wait_release(q.head_release(), &cancel).await;
2995
2996 // 4. `None` parks until cancellation rather than resolving at once.
2997 wait_release(None, &cancel).await;
2998 }
2999
3000 #[tokio::test]
3001 async fn the_pipe_loop_shape_from_the_contract_compiles_and_keeps_order() {
3002 // The pipe loop's `select!`, with the read half standing in for
3003 // `recv.read` and a recording sink for `send`. What it proves is
3004 // the shape: both branch expressions are borrow-free, the read
3005 // guard and the release branch coexist, and a delayed head holds
3006 // everything behind it.
3007 let (mut pending, counters) = queue();
3008 let cancel = CancellationToken::new();
3009 let mut sink = RecordingSink::default();
3010 let (tx, mut rx) = tokio::sync::mpsc::channel::<Bytes>(8);
3011
3012 tokio::spawn(async move {
3013 for chunk in [&b"0"[..], b"1", b"2", b"3"] {
3014 tx.send(Bytes::from_static(chunk)).await.expect("receiver lives");
3015 }
3016 // Stay open past the head's release time, or the source FINs
3017 // first and the FIN drain — not the release branch — is what
3018 // writes everything.
3019 tokio::time::sleep(Duration::from_millis(200)).await;
3020 });
3021
3022 let mut first = true;
3023 loop {
3024 let can_read = pending.accepts_more();
3025 let head_release = pending.head_release();
3026
3027 tokio::select! {
3028 chunk = rx.recv(), if can_read => {
3029 match chunk {
3030 Some(raw) => {
3031 // Object 0 is delayed; 1.. are not, and must
3032 // still not overtake it.
3033 let release_at = if std::mem::take(&mut first) {
3034 Instant::now() + Duration::from_millis(40)
3035 } else {
3036 Instant::now()
3037 };
3038 if pending.is_empty() && release_at <= Instant::now() {
3039 // Today's fast path: written inline in the read arm.
3040 write_unit(Pending::bytes(raw, release_at), &mut sink)
3041 .await
3042 .expect("recording sink");
3043 } else {
3044 pending.push(Pending::bytes(raw, release_at));
3045 }
3046 }
3047 None => {
3048 let outcome =
3049 drain_honouring_release_times(&mut pending, &mut sink, &cancel, no_shape_reports)
3050 .await
3051 .expect("recording sink");
3052 assert_eq!(outcome, DrainOutcome::Complete);
3053 break;
3054 }
3055 }
3056 }
3057 () = wait_release(head_release.clone(), &cancel), if head_release.is_some() => {
3058 let now = Instant::now();
3059 while let Some(unit) = pending.pop_next_due(now) {
3060 pending.record_release(&unit, now);
3061 write_unit(unit, &mut sink).await.expect("recording sink");
3062 }
3063 }
3064 () = cancel.cancelled() => unreachable!("nothing cancels this test"),
3065 }
3066 }
3067
3068 assert_eq!(
3069 sink.written(),
3070 Bytes::from_static(b"0123"),
3071 "byte equality is the ordering assertion"
3072 );
3073 let snap = counters.snapshot();
3074 assert_eq!(snap.egress_items_queued, 4, "every unit went through the deque");
3075 assert_eq!(
3076 snap.release_errors.count, 4,
3077 "all four share the head's clamped release time, so one wake pops all four \
3078 — and only the release branch samples",
3079 );
3080 }
3081
3082 #[test]
3083 fn defer_by_is_a_deadline_and_clamps_to_max_hold() {
3084 let config =
3085 EgressConfig { max_hold: Duration::from_millis(100), ..EgressConfig::default() };
3086 let now = Instant::now();
3087
3088 let short = defer_by(now, Duration::from_millis(10), &config);
3089 assert_eq!(short.release_at, now + Duration::from_millis(10));
3090 assert!(!short.was_clamped());
3091
3092 let long = defer_by(now, Duration::from_secs(5), &config);
3093 assert_eq!(long.release_at, now + Duration::from_millis(100));
3094 assert!(long.was_clamped());
3095 assert_eq!(long.requested, Duration::from_secs(5));
3096 assert_eq!(long.applied, Duration::from_millis(100));
3097
3098 // Two units arriving together with the same `by` release together,
3099 // not at +by and +2by.
3100 let a = defer_by(now, Duration::from_millis(10), &config);
3101 let b = defer_by(now, Duration::from_millis(10), &config);
3102 assert_eq!(a.release_at, b.release_at);
3103
3104 assert_eq!(hold_ceiling(now, &config), now + Duration::from_millis(100));
3105 }
3106
3107 #[test]
3108 fn the_first_close_request_wins() {
3109 let cancel = CancellationToken::new();
3110 let closer = SessionCloser::new(cancel.clone());
3111 assert!(!closer.is_closing());
3112 assert_eq!(
3113 closer.close_args(),
3114 (0, Bytes::from_static(b"proxy session ended")),
3115 "the default is what run_with_transport has always sent",
3116 );
3117
3118 assert!(closer.request(3, Bytes::from_static(b"protocol violation")));
3119 assert!(!cancel.is_cancelled(), "recording a close does not yet end the session");
3120 closer.cancel();
3121 assert!(cancel.is_cancelled());
3122 assert!(closer.is_closing());
3123 assert!(!closer.request(1, Bytes::from_static(b"too late")));
3124 assert_eq!(closer.close_args(), (3, Bytes::from_static(b"protocol violation")));
3125 assert_eq!(
3126 closer.requested(),
3127 Some((3, Bytes::from_static(b"protocol violation"), CloseOrigin::Hook)),
3128 "a close that arrived through `request` is a hook's, and the session's own \
3129 `SessionEnded` reason says so in those words",
3130 );
3131 }
3132
3133 #[test]
3134 fn an_undelayed_head_arms_a_deadline_that_never_touches_the_wheel() {
3135 // `arm_at` on an already-passed instant returns an
3136 // already-cancelled `Deadline` without constructing the wheel, so
3137 // a session that never delays never links it into executed code.
3138 //
3139 // Asserted on the deadline's own token rather than on
3140 // `release_timer::started()`: that is process-global state, and
3141 // another test in this binary may legitimately have started the
3142 // wheel on another thread between the two reads.
3143 let (mut q, _c) = queue();
3144 q.push(Pending::bytes(Bytes::from_static(b"now"), Instant::now()));
3145 let release = q.head_release().expect("one unit queued");
3146 assert!(
3147 release.deadline().token().is_cancelled(),
3148 "an already-due head must not be registered with the wheel",
3149 );
3150
3151 let (mut q, _c) = queue();
3152 q.push(Pending::bytes(
3153 Bytes::from_static(b"later"),
3154 Instant::now() + Duration::from_secs(60),
3155 ));
3156 let release = q.head_release().expect("one unit queued");
3157 assert!(!release.deadline().token().is_cancelled(), "a future head is registered");
3158 assert!(release_timer::started(), "…and registering is what starts the wheel");
3159 }
3160
3161 // ── The session-wide byte gauge ─────────────────────────────────
3162
3163 /// A queue reporting into a gauge, plus the gauge.
3164 fn gauged() -> (PendingQueue, Arc<EgressGauge>) {
3165 let gauge = EgressGauge::new();
3166 let q = PendingQueue::new(EgressConfig::default(), Arc::new(Recorder::new()))
3167 .with_gauge(Arc::clone(&gauge));
3168 (q, gauge)
3169 }
3170
3171 /// The gauge is the sum of what the queues hold, on every route bytes
3172 /// take out of one.
3173 ///
3174 /// It has to be exact in both directions or the one caller that reads
3175 /// it — a requested close deciding whether it may stop waiting —
3176 /// either stops early on bytes that are still queued, or waits out its
3177 /// whole window on bytes that left long ago.
3178 #[tokio::test]
3179 async fn the_gauge_follows_every_route_bytes_leave_a_queue_by() {
3180 let (mut q, gauge) = gauged();
3181 let now = Instant::now();
3182 assert_eq!(gauge.queued(), 0);
3183
3184 q.push(Pending::bytes(Bytes::from_static(b"aaa"), now));
3185 q.push(Pending::bytes(Bytes::from_static(b"bb"), now));
3186 assert_eq!(gauge.queued(), 5, "two pushes, five bytes");
3187
3188 // Route one: released at its release time.
3189 q.pop_next_due(now).expect("both are due");
3190 assert_eq!(gauge.queued(), 2);
3191
3192 // Route two: flushed by the teardown drain.
3193 let mut sink = RecordingSink::default();
3194 assert_eq!(q.drain_ignoring_release_times(&mut sink).await, DrainOutcome::Complete);
3195 assert_eq!(gauge.queued(), 0);
3196
3197 // Route three: cleared wholesale, as a terminal or a shaping reset
3198 // does.
3199 q.push(Pending::bytes(Bytes::from_static(b"cccc"), now));
3200 assert_eq!(gauge.queued(), 4);
3201 q.clear();
3202 assert_eq!(gauge.queued(), 0);
3203
3204 // Route four: the stream ends holding bytes.
3205 q.push(Pending::bytes(Bytes::from_static(b"dddd"), now));
3206 assert_eq!(gauge.queued(), 4);
3207 drop(q);
3208 assert_eq!(
3209 gauge.queued(),
3210 0,
3211 "a queue that goes away takes its remainder with it, or every later close waits out \
3212 its whole window"
3213 );
3214 }
3215
3216 /// The wait ends when the queues empty, and answers zero.
3217 #[tokio::test]
3218 async fn the_drain_wait_ends_the_moment_the_queues_empty() {
3219 let (mut q, gauge) = gauged();
3220 let now = Instant::now();
3221 q.push(Pending::bytes(Bytes::from_static(b"payload"), now));
3222
3223 // A generous window, so finishing quickly can only be the queue
3224 // emptying and not the deadline.
3225 let waiting = tokio::spawn({
3226 let gauge = Arc::clone(&gauge);
3227 async move { gauge.wait_idle(Duration::from_secs(30)).await }
3228 });
3229
3230 tokio::task::yield_now().await;
3231 let mut sink = RecordingSink::default();
3232 assert_eq!(q.drain_ignoring_release_times(&mut sink).await, DrainOutcome::Complete);
3233
3234 let stranded = tokio::time::timeout(Duration::from_secs(5), waiting)
3235 .await
3236 .expect("the wait must end on the queue emptying, not on its own deadline")
3237 .expect("the waiting task must not panic");
3238 assert_eq!(stranded, 0, "nothing was left, so nothing is abandoned");
3239 }
3240
3241 /// The wait ends at its deadline holding the exact residue, and an
3242 /// expired window makes the teardown flush write nothing.
3243 ///
3244 /// The second half is what keeps the arithmetic closed. A flush at that
3245 /// point hands bytes to a connection that is about to discard its
3246 /// buffer, so they are neither confirmably delivered nor confirmably
3247 /// lost; declining to write leaves `unconfirmed_bytes` equal to what
3248 /// was abandoned, exactly.
3249 #[tokio::test]
3250 async fn an_expired_window_reports_the_residue_and_writes_nothing() {
3251 let (mut q, gauge) = gauged();
3252 let now = Instant::now();
3253 q.push(Pending::bytes(Bytes::from_static(b"held"), now + Duration::from_secs(60)));
3254
3255 let stranded = gauge.wait_idle(Duration::from_millis(20)).await;
3256 assert_eq!(stranded, 4, "the window closed on four bytes that had not been released");
3257
3258 gauge.begin_discarding();
3259 let mut sink = RecordingSink::default();
3260 assert_eq!(
3261 q.drain_ignoring_release_times(&mut sink).await,
3262 DrainOutcome::Discarded,
3263 "past the deadline the flush declines rather than best-efforts"
3264 );
3265 assert!(sink.writes.is_empty(), "nothing reached the transport");
3266 assert_eq!(
3267 q.unconfirmed_bytes(),
3268 4,
3269 "so the reported figure is what was abandoned, with nothing handed anywhere"
3270 );
3271 assert_eq!(
3272 q.queued_bytes(),
3273 q.unconfirmed_bytes(),
3274 "and the two agree, which is the point"
3275 );
3276 }
3277
3278 /// A close recorded on the closer does **not** end the session.
3279 ///
3280 /// A hook's close is cancelled by `exec::execute` the moment it has
3281 /// been reported, because the hook asked for the session to end. A
3282 /// control-plane close has to fix the pair first and let the session
3283 /// keep running through its drain window, and it would have no window
3284 /// at all if recording cancelled.
3285 ///
3286 /// It also checks who the close is *reported as*.
3287 /// `run_with_transport` turns the recorded triple into the prose on
3288 /// [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded),
3289 /// and both callers reach the same `OnceLock`, so without an origin
3290 /// beside the pair every operator-initiated close was reported to
3291 /// observers as a hook's decision.
3292 #[test]
3293 fn recording_a_close_leaves_the_session_running() {
3294 let cancel = CancellationToken::new();
3295 let closer = SessionCloser::new(cancel.clone());
3296
3297 assert!(closer.record(7, Bytes::from_static(b"asked")));
3298 assert!(!cancel.is_cancelled(), "the drain window has not even started yet");
3299 assert_eq!(closer.close_args(), (7, Bytes::from_static(b"asked")));
3300
3301 // First writer wins here exactly as it does through `request`, so a
3302 // hook and a control plane racing cannot end up with a code from
3303 // one and a reason from the other.
3304 assert!(!closer.record(9, Bytes::from_static(b"second")));
3305 assert!(!closer.request(9, Bytes::from_static(b"second")));
3306 assert_eq!(closer.close_args(), (7, Bytes::from_static(b"asked")));
3307 assert!(!cancel.is_cancelled(), "neither recording call ends the session");
3308
3309 // The losing `request` above is a hook's, and it lost. The origin
3310 // has to lose with it: a session whose close was recorded by the
3311 // control plane and then re-asked for by a hook must not report the
3312 // hook's name against the control plane's code and reason, which is
3313 // the mislabel this field exists to stop.
3314 assert_eq!(
3315 closer.requested(),
3316 Some((7, Bytes::from_static(b"asked"), CloseOrigin::ControlPlane)),
3317 );
3318 }
3319}