moqtap_proxy/exec.rs
1//! The one place an [`Action`] becomes bytes on the wire, or a refusal.
2//!
3//! # Why this module exists at all
4//!
5//! Everything the engine can do is already published, by draft and by site,
6//! in [`crate::capability`]. This module's whole job is to *not* re-derive
7//! any of it: every admission decision here goes through
8//! [`crate::capability::classify`], the same function
9//! [`Capabilities::supports`](crate::capability::Capabilities::supports)
10//! publishes, so the table a caller reads before a run and the code
11//! that runs it are literally the same code. A cell that the table calls
12//! `No(WrongSite { .. })` is refused here *with the refusal value classify
13//! returned*, not with one this file rebuilt from the [`Action`] it was
14//! handed.
15//!
16//! That last distinction is load-bearing and easy to lose. At
17//! [`Site::Object`] both [`ActionKind::Replace`] and
18//! [`ActionKind::ReplaceObject`] classify to the **same** value —
19//! `No(WrongSite { site: Object, action: ActionKind::ReplaceObject })` —
20//! because `Action::Replace(b)` is the one action value that attempts both
21//! kinds at once. If this file synthesized `WrongSite { action:
22//! Replace }` from the value it saw, the published `ReplaceObject` cell would
23//! become unassertable by `tests/action_matrix.rs`, which compares
24//! `refusal == r`. The [`ProxyEvent::ActionRefused`] event's separate
25//! `action` field still reports what the value *was* — `Replace` — because
26//! that is what the hook returned; only `refusal` is the table's.
27//!
28//! # Which refusals are this module's to produce
29//!
30//! Three, and they are the three that depend on the action's payload or on
31//! session state rather than on the `(site, kind)` pair:
32//!
33//! * [`Refusal::WrongComposition`] — what a [`Action::Delay`] /
34//! [`Action::Hold`] wrapped;
35//! * [`Refusal::ErrorCodeOutOfRange`] — a stream error code above the QUIC
36//! varint ceiling;
37//! * [`Refusal::SessionAlreadyClosing`] — a second
38//! [`Action::CloseSession`].
39//!
40//! Everything else is propagated from `classify`. The **table-only**
41//! refusal [`Refusal::StreamNotFramed`] is never emitted from here —
42//! `every_refusal_this_module_emits_is_classifys_or_one_of_its_own_three`
43//! below is the falsifiable form of that claim: it sweeps fourteen drafts ×
44//! five sites × thirteen action shapes and asserts that neither variant ever
45//! reaches an `ActionRefused`, and that all three executor-owned refusals do.
46//!
47//! # Event cardinality — what this module guarantees
48//!
49//! * Every **refusal** emits exactly one [`ProxyEvent::ActionRefused`] and
50//! bumps `Counters::actions_refused` exactly once. The two happen in one
51//! function ([`Reporter::refused`]) so they cannot drift, and the counter
52//! is bumped even when the observer is detached — a run with no observer
53//! still counts its refusals.
54//! * Every **applied** action emits exactly one [`ProxyEvent::ActionApplied`]
55//! *per phase*.
56//! * Every **transport failure on an admitted action** emits exactly one
57//! [`ProxyEvent::ActionFailed`], through [`Reporter::failed`]. This module
58//! does not own the transport, so the caller makes that call; it is the
59//! only shape of report this module publishes rather than performs.
60//!
61//! ## `Delay { then: Replace }` — two events, and this is the ruling
62//!
63//! Nothing about the action shape forces the choice, so it is written down
64//! here. It is **two** [`ProxyEvent::ActionApplied`] events, distinguishable
65//! by their `action` field:
66//!
67//! 1. at the decision: `{ action: Delay, effect: Queued { release_at } }` —
68//! the modifier was accepted and the unit is in the queue;
69//! 2. at the release: `{ action: Replace, effect: Replaced { bytes } }` —
70//! the wire actually changed.
71//!
72//! One event would force a choice between reporting the deferral and reporting
73//! the effect, and a queued unit lost at teardown would have reported a
74//! `Replaced` that never happened. Two keep **the engine accepted this** and
75//! *"the wire changed"* separately falsifiable, which is exactly what
76//! [`ImpairmentKind::QueuedBytesAtTeardown`] is paired against. Stated as the
77//! rule a test can count: *one `ActionApplied` for a direct action, two for a
78//! `Delay`/`Hold`*.
79//!
80//! The second event is owed by this module and paid by the caller, because
81//! the release happens in `session.rs`'s `select!` arm long after `execute`
82//! returned. [`DeferredEffects`] is the ledger: `execute` pushes exactly one
83//! entry per [`PendingQueue::push`] and the caller pops exactly one per
84//! released unit, then hands it to [`Reporter::applied_deferred`]. Entries
85//! are `Option` because a *direct* action queued merely for ordering (a
86//! `Pass` behind a delayed unit) reports once at the decision and owes
87//! nothing at release.
88//!
89//! **Units a drain could not flush report nothing further** — that absence
90//! is the designed pairing with `QueuedBytesAtTeardown`, not a lost event.
91//! See [`DeferredEffects`] for the three-call discipline.
92
93use std::collections::VecDeque;
94use std::time::{Duration, Instant};
95
96use bytes::Bytes;
97
98use moqtap_codec::version::DraftVersion;
99
100use crate::action::{Action, DropMode, StreamAction};
101use crate::capability::{classify, ActionKind, CapCtx, Precondition, Refusal, Site, Support};
102use crate::egress::{self, Deferral, Pending, PendingQueue, SessionCloser, Terminal};
103use crate::event::{Effect, ImpairmentKind, ProxyEvent, SessionId};
104use crate::instrument::Recorder;
105use crate::observer::ProxyObserver;
106use crate::shape::StreamKey;
107use crate::types::{DataStreamType, ObjectMeta, ProxySide};
108
109/// The QUIC varint ceiling, `2^62 - 1`.
110///
111/// A stream application error code above it cannot be encoded, and quinn
112/// would panic or truncate rather than refuse. Checked here, before
113/// anything is sent, so the stream stays usable
114/// ([`Refusal::ErrorCodeOutOfRange`]).
115pub(crate) const MAX_APPLICATION_ERROR_CODE: u64 = (1u64 << 62) - 1;
116
117// ── What an action is being applied to ──────────────────────────────
118
119/// The unit an [`Action`] was returned for, with every fact
120/// [`crate::capability::classify`] needs to judge it.
121///
122/// An enum rather than a struct of `Option`s so that an under-populated
123/// [`CapCtx`] is not expressible: each variant carries exactly the facts its
124/// site's rules read, and [`Self::cap_ctx`] is the only place they are
125/// assembled. That is what makes `Support::Conditional` unreachable at
126/// execution time for the four *fact* preconditions — see
127/// [`admit_conditional`].
128///
129/// The two [`StreamAction`] sites are deliberately **not** here: they take
130/// [`execute_stream`], so `execute` cannot be called at a site whose hook
131/// method returns the other type, and `Support::NotAttemptable` is
132/// structurally unreachable from both entry points.
133#[derive(Debug)]
134pub(crate) enum Target<'a> {
135 /// A whole control-stream frame, with its wire bytes.
136 Control {
137 /// The frame's complete wire bytes.
138 raw: Bytes,
139 },
140 /// One framed object, with its wire bytes.
141 Object {
142 /// The object's framing, from the framer.
143 meta: &'a ObjectMeta,
144 /// The stream header's two subgroup-ID mode bits, on drafts 15-19.
145 ///
146 /// `None` on 07-14, whose header types carry no such pair. Supplying
147 /// it on 15-19 is what separates [`Refusal::ReservedHeaderMode`] from
148 /// [`Refusal::WouldRedefineSubgroupId`].
149 subgroup_id_mode: Option<u8>,
150 /// The object's complete wire bytes, framing and payload.
151 raw: Bytes,
152 },
153 /// One datagram.
154 Datagram {
155 /// The datagram's complete wire bytes.
156 raw: Bytes,
157 /// `data.len() - cursor.len()` after a **successful**
158 /// `AnyDatagramHeader::decode`, or `None` when the header did not
159 /// decode.
160 ///
161 /// This is the raw offset, not the verdict:
162 /// [`Self::payload_delimited`] applies draft-14's and the status
163 /// datagram's exceptions on top of it.
164 header_len: Option<usize>,
165 /// Whether the datagram carries an Object Status.
166 is_status: bool,
167 },
168 /// A stream ending. Carries no bytes.
169 StreamEnd {
170 /// `true` selects the control-stream rules for this site, `false`
171 /// the data-stream rules; they differ, so the flag is not cosmetic.
172 is_control_stream: bool,
173 },
174}
175
176impl Target<'_> {
177 /// Which published site this unit is at.
178 pub(crate) fn site(&self) -> Site {
179 match self {
180 Target::Control { .. } => Site::Control,
181 Target::Object { .. } => Site::Object,
182 Target::Datagram { .. } => Site::Datagram,
183 Target::StreamEnd { .. } => Site::StreamEnd,
184 }
185 }
186
187 /// The unit's wire bytes, or `None` at a site that has no unit.
188 fn raw(&self) -> Option<Bytes> {
189 match self {
190 Target::Control { raw } | Target::Object { raw, .. } | Target::Datagram { raw, .. } => {
191 Some(raw.clone())
192 }
193 Target::StreamEnd { .. } => None,
194 }
195 }
196
197 /// Whether a payload-preserving splice has a locatable boundary.
198 ///
199 /// At the object site, always: the payload is the trailing field in
200 /// every layout on all fourteen drafts and both stream kinds.
201 ///
202 /// At the datagram site this is where the three exceptions live, and
203 /// they live *here* rather than at the call site because getting one
204 /// wrong is silent. `header_len` is `data.len() - cursor.len()`, which
205 /// is a real boundary only when the decode both succeeded and left the
206 /// payload behind:
207 ///
208 /// * **draft-14** — `AnyDatagramHeader` there is `DatagramObject`, whose
209 /// `decode` ends by reading all remaining bytes, so `header_len`
210 /// is the whole datagram and splicing would emit
211 /// `header ++ old ++ new`;
212 /// * **a status datagram** — no payload slot exists at all;
213 /// * **`header_len: None`** — the hook fires on an undecodable datagram,
214 /// and there is nothing to splice after.
215 fn payload_delimited(&self, draft: DraftVersion) -> Option<bool> {
216 match self {
217 Target::Object { .. } => Some(true),
218 Target::Datagram { header_len, is_status, .. } => {
219 Some(header_len.is_some() && draft != DraftVersion::Draft14 && !*is_status)
220 }
221 Target::Control { .. } | Target::StreamEnd { .. } => None,
222 }
223 }
224
225 /// Where the payload starts, when [`Self::payload_delimited`] is true.
226 fn payload_offset(&self, draft: DraftVersion) -> Option<usize> {
227 if self.payload_delimited(draft) != Some(true) {
228 return None;
229 }
230 match self {
231 Target::Object { meta, raw, .. } => {
232 usize::try_from(meta.payload_len).ok().and_then(|n| raw.len().checked_sub(n))
233 }
234 Target::Datagram { header_len, .. } => *header_len,
235 Target::Control { .. } | Target::StreamEnd { .. } => None,
236 }
237 }
238
239 /// Assemble the facts [`crate::capability::classify`] reads.
240 ///
241 /// `replacement_len` is the only field that comes from the *action*
242 /// rather than from the unit, which is why it is a parameter.
243 fn cap_ctx(&self, draft: DraftVersion, replacement_len: Option<u64>) -> CapCtx {
244 let mut cx = CapCtx { draft: Some(draft), replacement_len, ..CapCtx::default() };
245 match self {
246 Target::Control { .. } => {}
247 Target::Object { meta, subgroup_id_mode, .. } => {
248 cx.stream_kind = Some(meta.stream_kind);
249 cx.index_in_stream = Some(meta.index_in_stream);
250 cx.subgroup_id_resolved = Some(meta.subgroup_id.is_some());
251 cx.is_status_object = Some(meta.status.is_some());
252 cx.payload_len = Some(meta.payload_len);
253 cx.payload_delimited = Some(true);
254 cx.subgroup_id_mode = *subgroup_id_mode;
255 }
256 Target::Datagram { is_status, .. } => {
257 cx.is_status_object = Some(*is_status);
258 cx.payload_delimited = self.payload_delimited(draft);
259 }
260 Target::StreamEnd { is_control_stream } => {
261 cx.is_control_stream = Some(*is_control_stream);
262 }
263 }
264 cx
265 }
266}
267
268/// Which of the two [`StreamAction`] sites a decision was taken at.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub(crate) enum StreamSite {
271 /// Between `accept_uni()` and `open_uni()`: no peer stream exists yet.
272 Open,
273 /// In the `FramerOut::Header` arm: the peer stream exists and has
274 /// carried no payload byte.
275 Header,
276}
277
278impl StreamSite {
279 /// The published site this maps to.
280 pub(crate) fn site(self) -> Site {
281 match self {
282 StreamSite::Open => Site::StreamOpen,
283 StreamSite::Header => Site::StreamHeader,
284 }
285 }
286}
287
288/// One unit of traffic, at one site, at one instant.
289#[derive(Debug)]
290pub(crate) struct Unit<'a> {
291 /// What the action applies to.
292 pub(crate) target: Target<'a>,
293 /// The draft this session is running as.
294 pub(crate) draft: DraftVersion,
295 /// When the unit arrived. [`Action::Delay`] is a deadline measured from
296 /// here, not from the moment the hook returned.
297 pub(crate) arrived_at: Instant,
298}
299
300// ── The engine state `execute` mutates ──────────────────────────────
301
302/// The per-stream deferral state.
303///
304/// The two halves travel together because [`DeferredEffects`] is only
305/// correct if it is pushed in lockstep with [`PendingQueue`]; making them
306/// one parameter is the cheapest way to keep a future edit from pushing to
307/// one and not the other.
308#[derive(Debug)]
309pub(crate) struct Queue<'a> {
310 /// The stream's pending deque.
311 pub(crate) pending: &'a mut PendingQueue,
312 /// The release-phase events owed against it.
313 pub(crate) deferred: &'a mut DeferredEffects,
314}
315
316/// Everything [`execute`] may mutate.
317#[derive(Debug)]
318pub(crate) struct Engine<'a> {
319 /// The stream's queue, or `None` at the datagram site.
320 ///
321 /// Datagrams are per-connection and unordered by definition, so they
322 /// have no queue — and [`Action::Delay`] / [`Action::Hold`] are
323 /// refused there, so nothing can need one. `None` is not a degraded
324 /// mode; it is the datagram site's shape.
325 pub(crate) queue: Option<Queue<'a>>,
326 /// Where [`Action::CloseSession`] is recorded.
327 pub(crate) closer: &'a SessionCloser,
328}
329
330impl Engine<'_> {
331 /// Whether a unit written now would overtake something already waiting —
332 /// **or** would escape the pacer.
333 ///
334 /// The second term is the whole of the release wiring's reach into this
335 /// module. `Plan::WriteNow` hands bytes straight to the transport, which
336 /// is correct and cheap on an unshaped stream and is exactly the path a
337 /// token bucket cannot see: `PendingQueue::pop_next_due` is the release
338 /// seam, and a unit that never enters the queue never reaches it. So a
339 /// shaped queue is *always* busy, and every unit on a shaped stream is
340 /// released rather than written.
341 ///
342 /// Nothing else in this module knows a class, a bucket or a discipline.
343 /// The queue tags what it is handed from the class the pipe loop last
344 /// resolved, so `push_unit` stays the one place a push and its ledger
345 /// entry happen together.
346 fn queue_is_busy(&self) -> bool {
347 self.queue.as_ref().is_some_and(|q| !q.pending.is_empty() || q.pending.is_shaped())
348 }
349}
350
351// ── The release-phase ledger ────────────────────────────────────────
352
353/// What a released unit owes the observer.
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub(crate) struct Deferred {
356 /// The **inner** action's kind — `Replace` for `Delay { then:
357 /// Replace(b) }`, which is what distinguishes this event from the
358 /// `Queued` one emitted at the decision.
359 pub(crate) action: ActionKind,
360 /// What the release did to the wire.
361 pub(crate) effect: Effect,
362}
363
364/// The [`ProxyEvent::ActionApplied`] events owed at release, in queue order.
365///
366/// A sibling FIFO of [`PendingQueue`] rather than a field on `Pending`,
367/// because `Pending` is `egress.rs`'s type and reporting is not its
368/// concern — that module deliberately emits no events at all.
369///
370/// # The three-call discipline
371///
372/// Exactly one entry is pushed by [`execute`] on every
373/// [`PendingQueue::push`], so `self.len() == pending.len()` at every point
374/// the caller can observe. In `session.rs`:
375///
376/// * **release arm** — one [`Self::pop`] per [`PendingQueue::pop_next_due`],
377/// and each `Some` goes to [`Reporter::applied_deferred`];
378/// * **after a `DrainOutcome::Complete`** — [`Self::take_all`], and every
379/// `Some` in it goes to `applied_deferred`: the drain wrote them, in
380/// order, and they are owed;
381/// * **after any other `DrainOutcome`** — [`Self::clear`], and *nothing* is
382/// emitted. Whatever the fallback could not flush is reported once as
383/// [`ImpairmentKind::QueuedBytesAtTeardown`] instead. The missing second
384/// event is the point: a unit lost at teardown must not have reported a
385/// `Replaced` that never reached the wire.
386#[derive(Debug, Default)]
387pub(crate) struct DeferredEffects {
388 q: VecDeque<Option<Deferred>>,
389}
390
391impl DeferredEffects {
392 /// An empty ledger. Allocates nothing until the first push.
393 pub(crate) fn new() -> Self {
394 Self::default()
395 }
396
397 /// How many entries are owed. Equal to `PendingQueue::len()`.
398 ///
399 /// Read only by the tests below, which assert exactly that equality —
400 /// the three-call discipline is what the pipe loops use, and none of
401 /// them needs a count. Kept rather than `#[cfg(test)]`d because the
402 /// equality is the ledger's whole invariant and a reader looking for it
403 /// should find the accessor beside it.
404 #[allow(dead_code)]
405 pub(crate) fn len(&self) -> usize {
406 self.q.len()
407 }
408
409 /// Whether anything is owed. Tests only — see [`Self::len`].
410 #[allow(dead_code)]
411 pub(crate) fn is_empty(&self) -> bool {
412 self.q.is_empty()
413 }
414
415 /// The entry for the unit that was just released.
416 pub(crate) fn pop(&mut self) -> Option<Deferred> {
417 self.q.pop_front().flatten()
418 }
419
420 /// Every entry still owed, in order. For a drain that completed.
421 pub(crate) fn take_all(&mut self) -> Vec<Deferred> {
422 std::mem::take(&mut self.q).into_iter().flatten().collect()
423 }
424
425 /// Forget everything owed. For a drain that did not complete.
426 pub(crate) fn clear(&mut self) {
427 self.q.clear();
428 }
429
430 /// Record one entry against one [`PendingQueue::push`].
431 fn push(&mut self, entry: Option<Deferred>) {
432 self.q.push_back(entry);
433 }
434}
435
436// ── Reporting ───────────────────────────────────────────────────────
437
438/// Where this module's events and counters go.
439///
440/// Holds the session identity so no call site has to restate it, and holds
441/// the [`Recorder`] so that a counter bump and its event are one function
442/// call apart at most. Borrowed rather than owned: it is built per call from
443/// `session.rs`'s `ForwardCtx`, which this module deliberately does not
444/// name.
445#[derive(Clone, Copy)]
446pub(crate) struct Reporter<'a> {
447 observer: &'a dyn ProxyObserver,
448 /// Cached `observer.wants_events()`. Gates **events only** — counters
449 /// are unconditional.
450 enabled: bool,
451 counters: &'a Recorder,
452 session_id: SessionId,
453 side: ProxySide,
454 stream_id: Option<u64>,
455}
456
457impl std::fmt::Debug for Reporter<'_> {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 f.debug_struct("Reporter")
460 .field("enabled", &self.enabled)
461 .field("session_id", &self.session_id)
462 .field("side", &self.side)
463 .field("stream_id", &self.stream_id)
464 .finish_non_exhaustive()
465 }
466}
467
468impl<'a> Reporter<'a> {
469 /// Build one for a stream direction, or for the datagram path
470 /// (`stream_id: None`).
471 pub(crate) fn new(
472 observer: &'a dyn ProxyObserver,
473 enabled: bool,
474 counters: &'a Recorder,
475 session_id: SessionId,
476 side: ProxySide,
477 stream_id: Option<u64>,
478 ) -> Self {
479 Self { observer, enabled, counters, session_id, side, stream_id }
480 }
481
482 /// One [`ProxyEvent::ActionApplied`], for one phase of one action — and
483 /// the counter, for the two kinds that have one.
484 ///
485 /// The bumps sit **outside** the `enabled` gate, on exactly the terms
486 /// [`Self::refused`] gives for its own: a session with nobody watching
487 /// still counts what it did, and a counter that moved only when someone
488 /// was looking would agree with its event by construction rather than by
489 /// measurement.
490 ///
491 /// **Each kind reaches this method once per unit it was applied to**,
492 /// which is what makes a match on the kind a count and not an
493 /// over-count. `check_composition` refuses `Delay` and `Hold` as an
494 /// inner action and refuses `Truncate` and `ResetStream` as a wrapped
495 /// one, so a composition can hold neither of these two; and
496 /// [`Self::applied_deferred`], which reports the **inner** kind when a
497 /// deferred unit is released, can therefore never name either of them.
498 /// A delayed unit is counted at its decision and reported again at its
499 /// release under whatever it was wrapping, and only the first of those
500 /// two is a `Delay`.
501 pub(crate) fn applied(&self, site: Site, action: ActionKind, effect: Effect) {
502 match action {
503 ActionKind::Delay => self.counters.note_unit_delayed(),
504 ActionKind::Truncate => self.counters.note_object_truncated(),
505 _ => {}
506 }
507 self.emit(ProxyEvent::ActionApplied {
508 session_id: self.session_id,
509 side: self.side,
510 stream_id: self.stream_id,
511 site,
512 action,
513 effect,
514 });
515 }
516
517 /// The release-phase half of a [`Action::Delay`] / [`Action::Hold`].
518 ///
519 /// Always at [`Site::Object`] or [`Site::Control`] — the two sites with
520 /// a queue — and always with the **inner** action's kind, which is what
521 /// tells it apart from the `Queued` event emitted at the decision.
522 pub(crate) fn applied_deferred(&self, site: Site, deferred: Deferred) {
523 self.applied(site, deferred.action, deferred.effect);
524 }
525
526 /// One [`ProxyEvent::ActionRefused`], and one `actions_refused`.
527 ///
528 /// The counter is bumped **outside** the `enabled` gate on purpose: a
529 /// session with no observer attached still counts what it refused, and
530 /// the counter and the event are asserted independently. Bumping it
531 /// inside would make the two agree only when someone was watching.
532 pub(crate) fn refused(&self, site: Site, action: ActionKind, refusal: Refusal) {
533 self.counters.note_action_refused();
534 self.emit(ProxyEvent::ActionRefused {
535 session_id: self.session_id,
536 side: self.side,
537 stream_id: self.stream_id,
538 site,
539 action,
540 refusal,
541 });
542 }
543
544 /// One [`ProxyEvent::ActionFailed`]: the action was admitted and the
545 /// transport rejected it.
546 ///
547 /// Called by `session.rs`, not from here — this module produces the
548 /// bytes and the plan, and the caller hands them to the transport, so
549 /// only the caller can see the rejection.
550 /// It **follows** an `ActionApplied` for the same attempt rather than
551 /// replacing one. [`execute`] emits the admission before it returns, and
552 /// the caller cannot un-emit it once the transport declines; the two
553 /// together say *the engine did it, and it did not arrive*, which is the
554 /// whole truth and neither event carries it alone. What this is exclusive
555 /// of is `ActionRefused`: a refused unit was never the engine's to place,
556 /// so a transport failure on its bytes is
557 /// [`ImpairmentKind::DatagramNotSent`] instead.
558 ///
559 /// Note the event carries no `stream_id`: it is the datagram path's, and
560 /// a datagram has no stream to name.
561 pub(crate) fn failed(&self, site: Site, action: ActionKind, error: String) {
562 self.emit(ProxyEvent::ActionFailed {
563 session_id: self.session_id,
564 side: self.side,
565 site,
566 action,
567 error,
568 });
569 }
570
571 /// One [`ProxyEvent::Impairment`], carrying the connection it is about.
572 ///
573 /// The leg is worked out here, from the kind, rather than being a
574 /// parameter each call site supplies. A site knows one thing — the
575 /// direction it reads from — and most of what this enum reports is a
576 /// failure to *write*, which belongs to the other connection. Asking
577 /// twenty sites to make that turn is asking for the one to get it wrong
578 /// that nothing downstream can catch: the event still arrives, still
579 /// carries a leg, and names the wrong one.
580 ///
581 /// See [`crate::event::impairment_leg`] for the table and for why four
582 /// kinds answer `None`.
583 ///
584 /// # Ordering
585 ///
586 /// Every caller of this is past the thing it is reporting: the reset has
587 /// been handed to the transport, the datagram has come back refused, the
588 /// queue has been abandoned. That is a rule about the call sites rather
589 /// than something this function can enforce, and it is stated on
590 /// [`ProxyEvent::Impairment`] because it is what an observer is entitled
591 /// to rely on.
592 pub(crate) fn impairment(&self, kind: ImpairmentKind) {
593 let leg = crate::event::impairment_leg(&kind, self.side);
594 self.emit(ProxyEvent::Impairment {
595 session_id: self.session_id,
596 side: self.side,
597 leg,
598 kind,
599 });
600 }
601
602 fn emit(&self, event: ProxyEvent) {
603 if self.enabled {
604 self.observer.on_event(&event);
605 }
606 }
607}
608
609// ── What the caller must do next ────────────────────────────────────
610
611/// The wire operation [`execute`] has decided on but not performed.
612///
613/// This module owns *what the bytes are*; `session.rs` owns the transport
614/// handle and the `await`. Keeping the split here is what lets every rule
615/// below be unit-tested with no QUIC, no runtime and no session — the same
616/// argument `egress.rs` makes for its [`egress::EgressSink`].
617#[derive(Debug, Clone, PartialEq, Eq)]
618pub(crate) enum Plan {
619 /// Hand these bytes to the transport now: `send.write_all` on a stream
620 /// site, `send_datagram` at the datagram site.
621 ///
622 /// A failure at the datagram site is a [`Reporter::failed`], not a
623 /// session teardown: one undeliverable datagram must not take the
624 /// session with it.
625 WriteNow(Bytes),
626 /// Nothing goes to the wire on this call. The unit was queued behind
627 /// something already waiting, elided, or the site carries no bytes.
628 Nothing,
629 /// A positional terminal is now at the tail of the queue. Drain
630 /// honouring release times; the drain returns
631 /// `DrainOutcome::Terminated { forwarded, code }` and the stream is
632 /// over — do not `finish()` it.
633 Terminal,
634 /// Do not forward this stream. Stop the source with `code`; at
635 /// [`StreamSite::Header`] also reset the destination, which already
636 /// exists and has carried nothing.
637 RejectStream {
638 /// The application error code.
639 code: u64,
640 },
641 /// Forward this stream, but do not call `dest.open_uni()` until `after`
642 /// has elapsed. Only [`StreamSite::Open`] can produce this — by the
643 /// header site the peer stream exists, and that site refuses
644 /// [`StreamAction::OpenAfter`] with
645 /// [`Refusal::WrongSite`](crate::capability::Refusal::WrongSite) rather
646 /// than accepting a deferral it cannot perform.
647 ///
648 /// `session.rs`'s unidirectional accept loop consumes it, and the
649 /// deferral reaches the wire: the stream is spawned with **no**
650 /// destination handle, and the per-stream task sleeps `after` — racing
651 /// the session's cancellation — before it opens one. Nothing is read
652 /// from the source in the meantime, so the peer sees no stream at all
653 /// for `after` and then a whole one.
654 ///
655 /// The open happens inside the task but still *before* the first source
656 /// byte is read, which is what keeps the two reject sites different on
657 /// this topology too: by the time the header decision is taken the peer
658 /// stream exists, so a rejection there still resets it.
659 OpenStreamAfter {
660 /// How long to wait before opening the peer stream.
661 after: Duration,
662 },
663 /// Forward this stream, but write nothing on it until `target` has
664 /// ended. An unknown or already-ended target proceeds immediately and
665 /// reports `SerializeTargetUnknown` once.
666 ///
667 /// Consumed at both stream sites, because it defers the first *write*
668 /// rather than the stream's existence: from the open site it is carried
669 /// into the pipe, which waits before reading; at the header site the
670 /// wait happens in the `FramerOut::Header` arm, ahead of the header's
671 /// own bytes. Either way the peer stream is already open and silent.
672 ///
673 /// One stream never waits: the unidirectional control stream of a draft
674 /// whose control plane is a pair of them. Holding its first write would
675 /// hold SETUP, and the session with it.
676 SerializeStreamAfter {
677 /// The stream this one waits on.
678 target: StreamKey,
679 },
680 /// A session close was recorded in the [`SessionCloser`], **and the
681 /// session token is already cancelled** — `SessionCloser::request`
682 /// cancels as it records, so the caller does not have to. Return from
683 /// the forwarding task; `run_with_transport` reads the code and reason
684 /// back out of the closer at `session.rs:255-256`.
685 ///
686 /// The unit itself is **not** forwarded: the hook returned a close
687 /// *instead of* an action on it.
688 CloseSession {
689 /// The session termination code.
690 code: u32,
691 /// The reason phrase.
692 reason: Bytes,
693 },
694}
695
696/// Everything one [`execute`] call decided.
697#[derive(Debug, Clone)]
698pub(crate) struct Outcome {
699 /// What the caller must do with the wire.
700 pub(crate) plan: Plan,
701 /// The effect that was reported, or the refusal that
702 /// was. **Both have already been emitted** — this is returned for the
703 /// caller's own bookkeeping and for tests, not as an instruction to
704 /// report again.
705 pub(crate) result: Result<Effect, Refusal>,
706 /// The caller must call `framer.note_elided(meta)` before polling the
707 /// framer again.
708 ///
709 /// True for an admitted [`DropMode::Elide`] at [`Site::Object`],
710 /// **including a deferred one**: the framer's cursor is positional and
711 /// `note_elided` asserts it names the object most recently emitted, so
712 /// it cannot be moved to the release.
713 pub(crate) note_elided: bool,
714 /// The [`Action::Delay`] arithmetic, on a `Delay` and only on a `Delay`.
715 ///
716 /// `Deferral::was_clamped()` says whether
717 /// [`crate::action::EgressConfig::max_hold`] cut the request short; when
718 /// it did, [`ImpairmentKind::HoldClamped`] has **already been emitted**.
719 /// Returned so a test can assert the arithmetic without reading the
720 /// observer. `None` for every other action, including [`Action::Hold`],
721 /// which carries no requested duration to clamp.
722 /// Read only by the tests below — the impairment it describes has already
723 /// been emitted, so `session.rs` has nothing left to do with it. It stays
724 /// on the returned value rather than being dropped because *the clamp is
725 /// assertable without an observer* is what makes the arithmetic gateable at
726 /// all.
727 #[allow(dead_code)]
728 pub(crate) clamped: Option<Deferral>,
729 /// Set on the push after which the queue stops accepting reads.
730 /// **Already reported** as [`ImpairmentKind::EgressQueueFull`], once per
731 /// stream. Read only by the tests below, for the same reason
732 /// [`Self::clamped`] is.
733 #[allow(dead_code)]
734 pub(crate) entered_backpressure: bool,
735}
736
737impl Outcome {
738 /// Whether the action was admitted.
739 pub(crate) fn is_applied(&self) -> bool {
740 self.result.is_ok()
741 }
742}
743
744// ── Entry points ────────────────────────────────────────────────────
745
746/// Execute one [`Action`] at one site.
747///
748/// Emits exactly one [`ProxyEvent::ActionApplied`] or exactly one
749/// [`ProxyEvent::ActionRefused`], plus any impairment the decision produced,
750/// and returns the wire operation the caller must perform.
751///
752/// **A refused unit is forwarded unchanged**, through the same
753/// queue-or-write-now fork an admitted `Pass` takes — a refusal must not let
754/// a unit overtake one already waiting.
755///
756/// # Order of checks
757///
758/// 1. [`crate::capability::classify`] on the `(site, kind)` pair. The site's own
759/// verdict comes first because it is the one the published table makes,
760/// and the table must win: `Delay { then: ResetStream }` at the datagram
761/// site is the datagram column's `WrongSite`, not a composition
762/// complaint.
763/// 2. Composition, for [`Action::Delay`] / [`Action::Hold`], which is
764/// validated **before the unit is queued** so a bad composition never
765/// reaches the deque and never bumps `egress_items_queued`.
766/// 3. The inner action, classified in its own right — `Delay { then:
767/// ReplacePayload(b) }` with the wrong length is
768/// [`Refusal::LengthChanged`], reported against the *inner* kind, which
769/// is the informative one.
770/// 4. The numeric code range, then the session-closing latch.
771pub(crate) fn execute(
772 unit: &Unit<'_>,
773 action: Action,
774 engine: &mut Engine<'_>,
775 report: &Reporter<'_>,
776) -> Outcome {
777 let site = unit.target.site();
778 match plan_action(unit, action, engine, report) {
779 Ok(applied) => {
780 report.applied(site, applied.action, applied.effect.clone());
781 Outcome {
782 plan: applied.plan,
783 result: Ok(applied.effect),
784 note_elided: applied.note_elided,
785 clamped: applied.clamped,
786 entered_backpressure: applied.entered_backpressure,
787 }
788 }
789 Err(Refused { action, refusal }) => {
790 report.refused(site, action, refusal.clone());
791 let (plan, entered_backpressure) = forward_unchanged(unit, engine, report);
792 Outcome {
793 plan,
794 result: Err(refusal),
795 note_elided: false,
796 clamped: None,
797 entered_backpressure,
798 }
799 }
800 }
801}
802
803/// Execute one [`StreamAction`], at [`Site::StreamOpen`] or
804/// [`Site::StreamHeader`].
805///
806/// A separate entry point rather than a variant of [`Target`], so that the
807/// two sites whose hook methods return [`StreamAction`] cannot be handed an
808/// [`Action`] and `Support::NotAttemptable` stays unreachable at runtime —
809/// a claim made falsifiable by observing **zero** events at those 30-odd
810/// site × action cells.
811pub(crate) fn execute_stream(
812 site: StreamSite,
813 draft: DraftVersion,
814 action: StreamAction,
815 report: &Reporter<'_>,
816) -> Outcome {
817 let published = site.site();
818 let cx = CapCtx { draft: Some(draft), ..CapCtx::default() };
819 // Every arm reports `ForwardedVerbatim` except the rejection: the two
820 // deferral decisions — `OpenAfter` and `SerializeAfter` — change *when*
821 // the stream exists or *when* it first writes, never a byte of it. A
822 // separate `Effect` would claim a content change that never happens.
823 let (kind, effect) = match action {
824 StreamAction::Open => (ActionKind::Open, Effect::ForwardedVerbatim),
825 StreamAction::Reject { code } => (ActionKind::Reject, Effect::StreamRejected { code }),
826 StreamAction::OpenAfter(_) => (ActionKind::OpenAfter, Effect::ForwardedVerbatim),
827 StreamAction::SerializeAfter(_) => (ActionKind::SerializeAfter, Effect::ForwardedVerbatim),
828 };
829
830 let refuse = |refusal: Refusal| {
831 report.refused(published, kind, refusal.clone());
832 Outcome {
833 plan: Plan::Nothing,
834 result: Err(refusal),
835 note_elided: false,
836 clamped: None,
837 entered_backpressure: false,
838 }
839 };
840
841 if let Err(refusal) = admit(classify(published, kind, &cx)) {
842 return refuse(refusal);
843 }
844 if let StreamAction::Reject { code } = action {
845 if let Err(refusal) = check_error_code(code) {
846 return refuse(refusal);
847 }
848 }
849
850 report.applied(published, kind, effect.clone());
851 Outcome {
852 plan: match action {
853 StreamAction::Open => Plan::Nothing,
854 StreamAction::Reject { code } => Plan::RejectStream { code },
855 StreamAction::OpenAfter(after) => Plan::OpenStreamAfter { after },
856 StreamAction::SerializeAfter(target) => Plan::SerializeStreamAfter { target },
857 },
858 result: Ok(effect),
859 note_elided: false,
860 clamped: None,
861 entered_backpressure: false,
862 }
863}
864
865/// Run the elide guards for a **shaper's** tail-drop, and say whether the
866/// unit may be discarded.
867///
868/// [`Overflow::DropTail`](crate::shape::Overflow::DropTail) is sound
869/// precisely because it discards the *arriving* unit at admission time,
870/// where `framer.note_elided` is still legal — the framer's positional
871/// cursor has not moved past the object, so the successor's delta fix-up
872/// can still be armed. That is the same place `Action::Drop(DropMode::Elide)`
873/// is judged, so it is judged by the same code: this function asks
874/// [`crate::capability::classify`] exactly what `prepare_content` asks it,
875/// and the three elide guards — `WouldRedefineSubgroupId`,
876/// `WouldDestroyStatusObject`, `ReservedHeaderMode` — apply unchanged.
877///
878/// # What a refusal means, and why it is reported
879///
880/// `false` means an elide guard refused, and the caller must **admit the
881/// unit anyway** — the queue overshoots its depth by one. A shaper may not
882/// corrupt a stream to honour a depth limit: eliding an object the framer
883/// cannot renumber around does not lose one object, it makes every
884/// successor decode with a wrong absolute ID on drafts 14-19.
885///
886/// The refusal is reported as an ordinary
887/// [`ProxyEvent::ActionRefused`] and bumps `Counters::actions_refused`,
888/// through the same [`Reporter::refused`] every other refusal takes. That
889/// is why any test asserting an exact drop count must assert
890/// `actions_refused == 0` in the same body: without it, the
891/// arrivals-minus-drops arithmetic is off by the number of refusals and
892/// "no guard fired" is hoped rather than checked.
893///
894/// # Why this is not `execute(.., Action::Drop(DropMode::Elide), ..)`
895///
896/// Because no hook returned one. Routing a configured drop through
897/// `execute` would emit `ActionApplied { action: DropElide }` per dropped
898/// unit — a per-object event claiming a hook decision that never happened,
899/// on a path whose reporting is capped at once per stream per outcome. What
900/// the shaper owes
901/// the observer is [`ProxyEvent::Shaped`], which the caller emits; what it
902/// owes on a *refusal* is the refusal, which is here.
903pub(crate) fn shape_elide(unit: &Unit<'_>, report: &Reporter<'_>) -> bool {
904 debug_assert!(
905 matches!(unit.target, Target::Object { .. }),
906 "only a framed object can be tail-dropped: the shaper never sees anything else",
907 );
908 match admit_kind(Site::Object, ActionKind::DropElide, unit, None) {
909 Ok(()) => {
910 report.counters.note_object_elided();
911 true
912 }
913 Err(Refused { action, refusal }) => {
914 report.refused(Site::Object, action, refusal);
915 false
916 }
917 }
918}
919
920// ── Planning ────────────────────────────────────────────────────────
921
922/// A refusal, and the kind to name in the event.
923///
924/// The two are separate because they disagree exactly once, and that
925/// disagreement is the point of the module note: `Action::Replace(b)` at
926/// `Site::Object` is refused with `WrongSite { action: ReplaceObject }` —
927/// `classify`'s value, which the published `ReplaceObject` cell is compared
928/// against — while the event says `action: Replace`, which is what the hook
929/// returned.
930#[derive(Debug, Clone)]
931struct Refused {
932 action: ActionKind,
933 refusal: Refusal,
934}
935
936impl Refused {
937 fn new(action: ActionKind, refusal: Refusal) -> Self {
938 Self { action, refusal }
939 }
940}
941
942/// An admitted action, before its event is emitted.
943#[derive(Debug)]
944struct AppliedPlan {
945 action: ActionKind,
946 effect: Effect,
947 plan: Plan,
948 note_elided: bool,
949 clamped: Option<Deferral>,
950 entered_backpressure: bool,
951}
952
953impl AppliedPlan {
954 fn simple(action: ActionKind, effect: Effect, plan: Plan) -> Self {
955 Self {
956 action,
957 effect,
958 plan,
959 note_elided: false,
960 clamped: None,
961 entered_backpressure: false,
962 }
963 }
964}
965
966/// What a *content* action makes of the unit — the four things a
967/// [`Action::Delay`] / [`Action::Hold`] may wrap, and the same four when
968/// they stand alone.
969#[derive(Debug)]
970struct Content {
971 kind: ActionKind,
972 payload: Payload,
973 effect: Effect,
974 note_elided: bool,
975}
976
977/// The bytes a content action produced, or their absence.
978#[derive(Debug, Clone, PartialEq, Eq)]
979enum Payload {
980 /// Write exactly these bytes.
981 Write(Bytes),
982 /// Write nothing. Still takes an ordering slot when the queue is busy:
983 /// the drop was decided at a point in the stream, and letting it out of
984 /// band would write a later unit before an earlier one.
985 Elide,
986 /// There is no unit. [`Site::StreamEnd`] only.
987 ///
988 /// Distinct from [`Self::Elide`] because a stream ending has no place in
989 /// the deque at all — pushing an empty ordering slot behind a delayed
990 /// object would owe a ledger entry for a unit that does not exist.
991 Absent,
992}
993
994fn plan_action(
995 unit: &Unit<'_>,
996 action: Action,
997 engine: &mut Engine<'_>,
998 report: &Reporter<'_>,
999) -> Result<AppliedPlan, Refused> {
1000 let site = unit.target.site();
1001 match action {
1002 Action::Delay { by, then } => {
1003 admit_kind(site, ActionKind::Delay, unit, None)?;
1004 check_composition(&then)?;
1005 // `prepare_content` first, and the clamp report strictly after
1006 // it. Every refusal a `Delay` can earn is taken above this line,
1007 // so an impairment emitted below it is reporting a hold that the
1008 // engine really did apply — see
1009 // `a_refused_action_reports_its_refusal_and_no_impairment`,
1010 // which is red the moment these two are swapped.
1011 let content = prepare_content(unit, &then, report)?;
1012 let config = queue_config(engine);
1013 let deferral = egress::defer_by(unit.arrived_at, by, &config);
1014 if deferral.was_clamped() {
1015 // `Some`, always: a `Delay` names its own duration, so this is
1016 // the arm of the report that has a figure to quote. The
1017 // absent case belongs to a shaped release whose bucket named
1018 // no refill instant at all.
1019 report.impairment(ImpairmentKind::HoldClamped {
1020 requested: Some(deferral.requested),
1021 applied: deferral.applied,
1022 });
1023 }
1024 let pending = pending_for(&content.payload, deferral.release_at);
1025 let push = push_unit(engine, report, pending, Some(content.deferred()));
1026 Ok(AppliedPlan {
1027 action: ActionKind::Delay,
1028 effect: Effect::Queued { release_at: push.release_at },
1029 plan: Plan::Nothing,
1030 note_elided: content.note_elided,
1031 clamped: Some(deferral),
1032 entered_backpressure: push.entered_backpressure,
1033 })
1034 }
1035 Action::Hold { gate, then } => {
1036 admit_kind(site, ActionKind::Hold, unit, None)?;
1037 check_composition(&then)?;
1038 let content = prepare_content(unit, &then, report)?;
1039 let config = queue_config(engine);
1040 let ceiling = egress::hold_ceiling(unit.arrived_at, &config);
1041 let pending = pending_for(&content.payload, ceiling).with_gate(gate);
1042 let push = push_unit(engine, report, pending, Some(content.deferred()));
1043 Ok(AppliedPlan {
1044 action: ActionKind::Hold,
1045 effect: Effect::Queued { release_at: push.release_at },
1046 plan: Plan::Nothing,
1047 note_elided: content.note_elided,
1048 clamped: None,
1049 entered_backpressure: push.entered_backpressure,
1050 })
1051 }
1052 Action::Truncate { bytes, code } => {
1053 admit_kind(site, ActionKind::Truncate, unit, None)?;
1054 check_error_code(code).map_err(|r| Refused::new(ActionKind::Truncate, r))?;
1055 let raw = unit.target.raw().unwrap_or_default();
1056 let prefix = raw.slice(..bytes.min(raw.len()));
1057 let forwarded = prefix.len();
1058 let push = push_unit(
1059 engine,
1060 report,
1061 Pending::terminal(Terminal::Truncate { prefix, code }),
1062 None,
1063 );
1064 Ok(AppliedPlan {
1065 action: ActionKind::Truncate,
1066 effect: Effect::Truncated {
1067 forwarded,
1068 code,
1069 code_defined: stream_reset_code_defined(unit.draft),
1070 },
1071 plan: Plan::Terminal,
1072 note_elided: false,
1073 clamped: None,
1074 entered_backpressure: push.entered_backpressure,
1075 })
1076 }
1077 Action::ResetStream { code } => {
1078 admit_kind(site, ActionKind::ResetStream, unit, None)?;
1079 check_error_code(code).map_err(|r| Refused::new(ActionKind::ResetStream, r))?;
1080 let push = push_unit(engine, report, Pending::terminal(Terminal::Reset { code }), None);
1081 Ok(AppliedPlan {
1082 action: ActionKind::ResetStream,
1083 effect: Effect::StreamReset {
1084 code,
1085 code_defined: stream_reset_code_defined(unit.draft),
1086 },
1087 plan: Plan::Terminal,
1088 note_elided: false,
1089 clamped: None,
1090 entered_backpressure: push.entered_backpressure,
1091 })
1092 }
1093 Action::CloseSession { code, reason } => {
1094 admit_kind(site, ActionKind::CloseSession, unit, None)?;
1095 if !engine.closer.request(code, reason.clone()) {
1096 return Err(Refused::new(ActionKind::CloseSession, Refusal::SessionAlreadyClosing));
1097 }
1098 Ok(AppliedPlan::simple(
1099 ActionKind::CloseSession,
1100 Effect::SessionClosing { code },
1101 Plan::CloseSession { code, reason },
1102 ))
1103 }
1104 content_action => {
1105 let content = prepare_content(unit, &content_action, report)?;
1106 let (plan, entered_backpressure) = commit_now(&content.payload, engine, report);
1107 Ok(AppliedPlan {
1108 action: content.kind,
1109 effect: content.effect,
1110 plan,
1111 note_elided: content.note_elided,
1112 clamped: None,
1113 entered_backpressure,
1114 })
1115 }
1116 }
1117}
1118
1119impl Content {
1120 /// The event this content owes when its unit is released.
1121 fn deferred(&self) -> Deferred {
1122 Deferred { action: self.kind, effect: self.effect.clone() }
1123 }
1124}
1125
1126/// Classify and realise one content action.
1127///
1128/// The only place [`Action::Pass`], [`Action::Replace`],
1129/// [`Action::ReplacePayload`] and [`Action::Drop`] turn into bytes, reached
1130/// both directly and through a [`Action::Delay`] / [`Action::Hold`].
1131fn prepare_content(
1132 unit: &Unit<'_>,
1133 action: &Action,
1134 report: &Reporter<'_>,
1135) -> Result<Content, Refused> {
1136 let site = unit.target.site();
1137 match action {
1138 Action::Pass => {
1139 admit_kind(site, ActionKind::Pass, unit, None)?;
1140 Ok(Content {
1141 kind: ActionKind::Pass,
1142 payload: match unit.target.raw() {
1143 Some(raw) => Payload::Write(raw),
1144 // `Site::StreamEnd`: passing a stream ending is today's
1145 // behaviour at all four FIN sites, and today's behaviour
1146 // is to do nothing.
1147 None => Payload::Absent,
1148 },
1149 effect: Effect::ForwardedVerbatim,
1150 note_elided: false,
1151 })
1152 }
1153 Action::Replace(replacement) => {
1154 admit_kind(site, ActionKind::Replace, unit, None)?;
1155 Ok(Content {
1156 kind: ActionKind::Replace,
1157 payload: Payload::Write(replacement.clone()),
1158 effect: Effect::Replaced { bytes: replacement.len() },
1159 note_elided: false,
1160 })
1161 }
1162 Action::ReplacePayload(replacement) => {
1163 let replacement_len = u64::try_from(replacement.len()).ok();
1164 admit_kind(site, ActionKind::ReplacePayload, unit, replacement_len)?;
1165 let raw = unit.target.raw().unwrap_or_default();
1166 // `admit_kind` returning `Ok` is what guarantees the offset
1167 // exists: `Precondition::DatagramPayloadDelimited` is the
1168 // datagram half and the object site is unconditionally
1169 // delimited. The `unwrap_or` is a total-function guard,
1170 // not a fallback with meaning.
1171 let offset = unit.target.payload_offset(unit.draft).unwrap_or(raw.len());
1172 let mut spliced = Vec::with_capacity(offset + replacement.len());
1173 spliced.extend_from_slice(&raw[..offset]);
1174 spliced.extend_from_slice(replacement);
1175 let spliced = Bytes::from(spliced);
1176 Ok(Content {
1177 kind: ActionKind::ReplacePayload,
1178 payload: Payload::Write(spliced.clone()),
1179 effect: Effect::Replaced { bytes: spliced.len() },
1180 note_elided: false,
1181 })
1182 }
1183 Action::Drop(DropMode::Elide) => {
1184 admit_kind(site, ActionKind::DropElide, unit, None)?;
1185 let at_object_site = matches!(unit.target, Target::Object { .. });
1186 let effect = if at_object_site {
1187 report.counters.note_object_elided();
1188 Effect::Elided { renumbered_successor: elide_renumbers_successor(unit) }
1189 } else {
1190 // A control frame or a datagram has no object slot to
1191 // renumber, so the mode is ignored rather than refused and
1192 // the honest report is that the unit is gone.
1193 Effect::Dropped
1194 };
1195 Ok(Content {
1196 kind: ActionKind::DropElide,
1197 payload: Payload::Elide,
1198 effect,
1199 note_elided: at_object_site,
1200 })
1201 }
1202 // Reachable only as a `Delay`/`Hold` inner action, and
1203 // `check_composition` rejects every one of these before we get
1204 // here. Kept total rather than panicking: a capability engine that
1205 // can panic is worse than one that repeats itself.
1206 Action::Delay { .. } | Action::Hold { .. } => {
1207 Err(Refused::new(kind_of(action), NESTED_MODIFIER))
1208 }
1209 Action::Truncate { .. } | Action::ResetStream { .. } => {
1210 Err(Refused::new(kind_of(action), WRAPPED_TERMINAL))
1211 }
1212 Action::CloseSession { .. } => Err(Refused::new(kind_of(action), WRAPPED_CLOSE)),
1213 }
1214}
1215
1216// ── Admission ───────────────────────────────────────────────────────
1217
1218/// Ask [`crate::capability::classify`], and turn its verdict into an admission.
1219fn admit_kind(
1220 site: Site,
1221 kind: ActionKind,
1222 unit: &Unit<'_>,
1223 replacement_len: Option<u64>,
1224) -> Result<(), Refused> {
1225 let cx = unit.target.cap_ctx(unit.draft, replacement_len);
1226 admit(classify(site, kind, &cx)).map_err(|refusal| Refused::new(kind, refusal))
1227}
1228
1229/// The five verdicts, as an admission decision.
1230///
1231/// `NotAttemptable` and `Unreachable` are structurally unreachable from both
1232/// entry points — the first because the two `StreamAction` sites take
1233/// [`execute_stream`] and the four constructor-less kinds have no `Action`
1234/// value, the second because the hook is never invoked on a bypassed stream.
1235/// They are handled rather than `unreachable!()`d for the same reason
1236/// `capability.rs` has `filtered_earlier`: a total function beats a
1237/// panicking one on a forwarding task.
1238fn admit(support: Support) -> Result<(), Refusal> {
1239 match support {
1240 Support::Yes => Ok(()),
1241 Support::No(refusal) => Err(refusal),
1242 Support::Conditional(precondition) => admit_conditional(precondition),
1243 Support::NotAttemptable { refusal, .. } | Support::Unreachable { refusal, .. } => {
1244 Err(refusal)
1245 }
1246 }
1247}
1248
1249/// What a surviving [`Support::Conditional`] means *at execution time*.
1250///
1251/// One of the five preconditions is environmental — it cannot be settled
1252/// from the unit, so the table publishes it as conditional forever and the
1253/// engine admits the action:
1254///
1255/// * [`Precondition::WithinMaxDatagramSize`] — no transport in the workspace
1256/// exposes a maximum datagram size, so the verdict arrives as
1257/// `send_datagram` failing, which is a [`Reporter::failed`], not a
1258/// refusal, and the session survives it.
1259///
1260/// The other four are *facts about the unit*, and [`Target`] supplies every
1261/// one of them, so reaching those arms means a `CapCtx` was assembled
1262/// somewhere other than [`Target::cap_ctx`].
1263/// `no_fact_precondition_survives_execution` sweeps every site × every
1264/// action with fully-populated targets and asserts they never arrive, which
1265/// is what makes the `debug_assert` a claim rather than a hope.
1266fn admit_conditional(precondition: Precondition) -> Result<(), Refusal> {
1267 match precondition {
1268 Precondition::WithinMaxDatagramSize => Ok(()),
1269 Precondition::ReplacementLengthEqualsPayload
1270 | Precondition::NotFirstObjectOfImplicitSubgroup
1271 | Precondition::NotAStatusObject
1272 | Precondition::DatagramPayloadDelimited => {
1273 debug_assert!(
1274 false,
1275 "exec supplies every per-unit fact; {precondition:?} means a CapCtx \
1276 was built outside Target::cap_ctx",
1277 );
1278 Ok(())
1279 }
1280 }
1281}
1282
1283/// *Composition*: a modifier accepts only a content action.
1284///
1285/// Checked **before the unit is queued**, so a bad composition never reaches
1286/// the deque and `egress_items_queued` does not move — which is what
1287/// `a_delay_wrapping_a_terminal_is_refused_before_it_is_queued` asserts.
1288///
1289/// The four legal inner actions are [`Action::Pass`],
1290/// [`Action::Replace`], [`Action::ReplacePayload`] and [`Action::Drop`].
1291/// [`Action::Drop`] belongs on it, and the fact that it does is worth one
1292/// sentence here because the `Action` rustdoc once said the opposite: a
1293/// deferred drop is **not** a no-op, because it holds an ordering slot for
1294/// the whole of its delay and [`PendingQueue::pop_next_due`] only ever
1295/// considers the front. The unit is deleted *and* the rest of the stream
1296/// is head-of-line-blocked, which is the impairment the composition exists
1297/// to express.
1298/// `a_delayed_drop_is_admitted_and_blocks_the_stream_behind_it` is the
1299/// falsifiable form of that claim — it asserts the successor's clamp, not
1300/// merely the admission, so a revision that queued the drop without an
1301/// ordering slot would still fail it.
1302fn check_composition(inner: &Action) -> Result<(), Refused> {
1303 let refusal = match inner {
1304 Action::Pass | Action::Replace(_) | Action::ReplacePayload(_) | Action::Drop(_) => {
1305 return Ok(())
1306 }
1307 Action::Delay { .. } | Action::Hold { .. } => NESTED_MODIFIER,
1308 Action::Truncate { .. } | Action::ResetStream { .. } => WRAPPED_TERMINAL,
1309 Action::CloseSession { .. } => WRAPPED_CLOSE,
1310 };
1311 Err(Refused::new(kind_of(inner), refusal))
1312}
1313
1314/// A stream application error code must fit a QUIC varint.
1315fn check_error_code(code: u64) -> Result<(), Refusal> {
1316 if code > MAX_APPLICATION_ERROR_CODE {
1317 Err(Refusal::ErrorCodeOutOfRange { code })
1318 } else {
1319 Ok(())
1320 }
1321}
1322
1323const NESTED_MODIFIER: Refusal =
1324 Refusal::WrongComposition { detail: "Delay or Hold wrapping another Delay or Hold" };
1325const WRAPPED_TERMINAL: Refusal =
1326 Refusal::WrongComposition { detail: "Delay or Hold wrapping Truncate or ResetStream" };
1327const WRAPPED_CLOSE: Refusal =
1328 Refusal::WrongComposition { detail: "Delay or Hold wrapping CloseSession" };
1329
1330// ── Committing to the wire or to the queue ──────────────────────────
1331
1332/// Write now, or take an ordering slot behind what is already waiting.
1333///
1334/// A unit that is due now on an **empty** queue is not pushed at all —
1335/// it is written inline in the read arm, which is today's code and today's
1336/// cost. On a **non-empty** queue it must be pushed, or it overtakes the
1337/// units ahead of it; the deque is what holds it back, and its own deadline
1338/// stays `now` (see [`PendingQueue::push`]) so that it goes out the instant
1339/// the units ahead of it have.
1340fn commit_now(payload: &Payload, engine: &mut Engine<'_>, report: &Reporter<'_>) -> (Plan, bool) {
1341 if matches!(payload, Payload::Absent) || !engine.queue_is_busy() {
1342 return (
1343 match payload {
1344 Payload::Write(raw) => Plan::WriteNow(raw.clone()),
1345 Payload::Elide | Payload::Absent => Plan::Nothing,
1346 },
1347 false,
1348 );
1349 }
1350 let push = push_unit(engine, report, pending_for(payload, Instant::now()), None);
1351 (Plan::Nothing, push.entered_backpressure)
1352}
1353
1354/// The refusal path's write: the unit goes out unchanged, through the same
1355/// fork an admitted [`Action::Pass`] takes.
1356fn forward_unchanged(
1357 unit: &Unit<'_>,
1358 engine: &mut Engine<'_>,
1359 report: &Reporter<'_>,
1360) -> (Plan, bool) {
1361 match unit.target.raw() {
1362 Some(raw) => commit_now(&Payload::Write(raw), engine, report),
1363 // `Site::StreamEnd` carries no unit: refusing there leaves the FIN
1364 // exactly as it was, which is `Pass`'s behaviour and today's.
1365 None => (Plan::Nothing, false),
1366 }
1367}
1368
1369fn pending_for(payload: &Payload, release_at: Instant) -> Pending {
1370 match payload {
1371 Payload::Write(raw) => Pending::bytes(raw.clone(), release_at),
1372 // `Absent` cannot be deferred: the only site that produces it,
1373 // `Site::StreamEnd`, refuses `Delay` and `Hold` outright, and
1374 // `commit_now` short-circuits it before it reaches here.
1375 Payload::Elide | Payload::Absent => Pending::elided(release_at),
1376 }
1377}
1378
1379/// Push one unit and record exactly one ledger entry against it.
1380///
1381/// The single place both happen, so `DeferredEffects::len() ==
1382/// PendingQueue::len()` holds by construction rather than by review.
1383fn push_unit(
1384 engine: &mut Engine<'_>,
1385 report: &Reporter<'_>,
1386 unit: Pending,
1387 owed: Option<Deferred>,
1388) -> egress::Push {
1389 let Some(queue) = engine.queue.as_mut() else {
1390 debug_assert!(
1391 false,
1392 "the datagram site has no queue, and Delay/Hold/Truncate/ResetStream \
1393 are refused there — nothing may reach a push",
1394 );
1395 return egress::Push { release_at: Instant::now(), entered_backpressure: false };
1396 };
1397 let push = queue.pending.push(unit);
1398 queue.deferred.push(owed);
1399 if push.entered_backpressure {
1400 if let Some(stream_id) = report.stream_id {
1401 report.impairment(ImpairmentKind::EgressQueueFull { stream_id });
1402 }
1403 }
1404 push
1405}
1406
1407/// Queue bytes the hook was never shown, on a **shaped** stream.
1408///
1409/// The counterpart of `session.rs`'s `write_in_order`, which drains and then
1410/// writes inline. That is right on an unshaped stream and wrong on a shaped
1411/// one twice over: it would let a stream header or an oversized object's
1412/// passthrough chunk escape the pacer, and — worse — the drain it runs first
1413/// honours release times, so on a paced queue it would block the read arm
1414/// for as long as the bucket took, inside a `select!` arm body that polls no
1415/// other branch.
1416///
1417/// Lives here rather than in `session.rs` for the reason `write_in_order`'s
1418/// own doc gives: `DeferredEffects`'s push is this module's, so the ledger
1419/// and the deque can only move together. The entry is `None` — these bytes
1420/// were never a hook decision, so nothing is owed at release.
1421///
1422/// The class is not a parameter: the queue carries the one the pipe loop
1423/// most recently resolved (`PendingQueue::tag_unit`), which for a header or
1424/// a passthrough chunk is `Class::Unshapeable`.
1425pub(crate) fn enqueue_unshown(
1426 pending: &mut PendingQueue,
1427 deferred: &mut DeferredEffects,
1428 raw: Bytes,
1429 report: &Reporter<'_>,
1430) {
1431 let push = pending.push(Pending::bytes(raw, Instant::now()));
1432 deferred.push(None);
1433 if push.entered_backpressure {
1434 if let Some(stream_id) = report.stream_id {
1435 report.impairment(ImpairmentKind::EgressQueueFull { stream_id });
1436 }
1437 }
1438}
1439
1440/// The queue's knobs, or the defaults when there is no queue.
1441///
1442/// The `None` arm is the datagram site, where `Delay` and `Hold` are refused
1443/// before they can ask — it exists so this is an expression rather than a
1444/// panic.
1445fn queue_config(engine: &Engine<'_>) -> crate::action::EgressConfig {
1446 const FALLBACK: crate::action::EgressConfig = crate::action::EgressConfig {
1447 max_pending_bytes: 1024 * 1024,
1448 max_hold: std::time::Duration::from_secs(30),
1449 // Never read here: the drain window belongs to a session close and
1450 // this fallback exists for the datagram site, which has no queue.
1451 // Restated rather than elided because `EgressConfig::default()` is
1452 // not a `const fn`, so this literal has to name every field.
1453 drain_timeout: std::time::Duration::from_millis(100),
1454 };
1455 match engine.queue.as_ref() {
1456 Some(queue) => *queue.pending.config(),
1457 None => FALLBACK,
1458 }
1459}
1460
1461// ── Per-draft facts this module needs ───────────────────────────────
1462
1463/// Whether the draft defines a stream-reset error code vocabulary.
1464///
1465/// Drafts 07-10 do not, so the reset still executes with the code the
1466/// action named and [`Effect::StreamReset`] / [`Effect::Truncated`] report
1467/// `code_defined: false` — the code is a choice there, not a claim.
1468///
1469/// Exhaustive rather than `!matches!(..)`. The negated form leans the other
1470/// way from the rest of these predicates — a draft nobody listed would be
1471/// *granted* a vocabulary rather than refused one, and the proxy would then
1472/// publish `code_defined: true` about a draft no one has read. Either default
1473/// is a guess; this one has to be written down.
1474const fn stream_reset_code_defined(draft: DraftVersion) -> bool {
1475 match draft {
1476 DraftVersion::Draft07
1477 | DraftVersion::Draft08
1478 | DraftVersion::Draft09
1479 | DraftVersion::Draft10 => false,
1480 DraftVersion::Draft11
1481 | DraftVersion::Draft12
1482 | DraftVersion::Draft13
1483 | DraftVersion::Draft14
1484 | DraftVersion::Draft15
1485 | DraftVersion::Draft16
1486 | DraftVersion::Draft17
1487 | DraftVersion::Draft18
1488 | DraftVersion::Draft19
1489 | DraftVersion::Draft20 => true,
1490 }
1491}
1492
1493/// Whether eliding this object leaves the framer owing a successor fix-up.
1494///
1495/// Mirrors `ObjectFramer::elide_owes_a_fixup`, which is private to
1496/// `framer.rs`, and the two stream kinds owe it from different drafts. A
1497/// **subgroup** stream delta-encodes object IDs from draft-14, so the one
1498/// object following an elided run has its leading ID varint rewritten. A
1499/// **fetch** stream owes it from draft-15, where a Serialization Flags field
1500/// lets a frame take any of its Group ID, Subgroup ID, Object ID and
1501/// Priority from the frame before it, and the payment is a re-encode of the
1502/// survivor's whole framing rather than a rewrite of one varint. Drafts
1503/// 07-13 subgroup streams and 07-14 fetch streams state every field
1504/// outright and owe nothing.
1505///
1506/// Duplicated rather than borrowed because the value is needed *before*
1507/// `framer.note_elided(meta)` is called — the effect is reported at the
1508/// decision, and `note_elided` is what arms the fix-up.
1509/// `elide_renumbering_names_the_drafts_that_owe_a_fixup` restates the table
1510/// explicitly; see its comment for why it cannot ask the framer directly,
1511/// and what covers the gap.
1512///
1513/// Both arms are exhaustive matches rather than `matches!`. `false` here means
1514/// *eliding this object costs the next one nothing*, which is the answer that
1515/// forwards a stream whose remaining Locations no longer decode — so a draft
1516/// that arrives without an answer must stop the build rather than take that
1517/// one. The two boundaries differ (subgroup from 14, fetch from 15), which is
1518/// exactly why neither can be extrapolated from the other.
1519fn elide_renumbers_successor(unit: &Unit<'_>) -> bool {
1520 let Target::Object { meta, .. } = &unit.target else {
1521 return false;
1522 };
1523 match meta.stream_kind {
1524 DataStreamType::Subgroup => match unit.draft {
1525 DraftVersion::Draft07
1526 | DraftVersion::Draft08
1527 | DraftVersion::Draft09
1528 | DraftVersion::Draft10
1529 | DraftVersion::Draft11
1530 | DraftVersion::Draft12
1531 | DraftVersion::Draft13 => false,
1532 DraftVersion::Draft14
1533 | DraftVersion::Draft15
1534 | DraftVersion::Draft16
1535 | DraftVersion::Draft17
1536 | DraftVersion::Draft18
1537 | DraftVersion::Draft19
1538 | DraftVersion::Draft20 => true,
1539 },
1540 DataStreamType::Fetch => match unit.draft {
1541 DraftVersion::Draft07
1542 | DraftVersion::Draft08
1543 | DraftVersion::Draft09
1544 | DraftVersion::Draft10
1545 | DraftVersion::Draft11
1546 | DraftVersion::Draft12
1547 | DraftVersion::Draft13
1548 | DraftVersion::Draft14 => false,
1549 DraftVersion::Draft15
1550 | DraftVersion::Draft16
1551 | DraftVersion::Draft17
1552 | DraftVersion::Draft18
1553 | DraftVersion::Draft19
1554 | DraftVersion::Draft20 => true,
1555 },
1556 }
1557}
1558
1559/// The [`ActionKind`] an [`Action`] value attempts.
1560///
1561/// `Action::Replace(b)` maps to [`ActionKind::Replace`] here even at
1562/// [`Site::Object`], where it also carries [`ActionKind::ReplaceObject`].
1563/// That is deliberate: this function answers *what the hook returned*, which
1564/// is the `ActionRefused` event's `action` field. The *refusal* comes from
1565/// `classify`, which names `ReplaceObject`, and the two are compared against
1566/// different things: the event field against what the hook returned, the
1567/// refusal against the published table.
1568const fn kind_of(action: &Action) -> ActionKind {
1569 match action {
1570 Action::Pass => ActionKind::Pass,
1571 Action::Replace(_) => ActionKind::Replace,
1572 Action::ReplacePayload(_) => ActionKind::ReplacePayload,
1573 Action::Delay { .. } => ActionKind::Delay,
1574 Action::Hold { .. } => ActionKind::Hold,
1575 Action::Drop(DropMode::Elide) => ActionKind::DropElide,
1576 Action::Truncate { .. } => ActionKind::Truncate,
1577 Action::ResetStream { .. } => ActionKind::ResetStream,
1578 Action::CloseSession { .. } => ActionKind::CloseSession,
1579 }
1580}
1581
1582#[cfg(test)]
1583mod tests {
1584 use std::sync::{Arc, Mutex};
1585 use std::time::Duration;
1586
1587 use super::*;
1588 use crate::action::{EgressConfig, Gate};
1589 use crate::egress::Item;
1590 use crate::types::Leg;
1591 use tokio_util::sync::CancellationToken;
1592
1593 // ── harness ─────────────────────────────────────────────────────
1594
1595 #[derive(Default)]
1596 struct Recording {
1597 events: Mutex<Vec<ProxyEvent>>,
1598 }
1599
1600 impl ProxyObserver for Recording {
1601 fn on_event(&self, event: &ProxyEvent) {
1602 self.events.lock().unwrap().push(event.clone());
1603 }
1604 }
1605
1606 impl Recording {
1607 fn events(&self) -> Vec<ProxyEvent> {
1608 self.events.lock().unwrap().clone()
1609 }
1610 fn applied(&self) -> Vec<(Site, ActionKind, Effect)> {
1611 self.events()
1612 .into_iter()
1613 .filter_map(|e| match e {
1614 ProxyEvent::ActionApplied { site, action, effect, .. } => {
1615 Some((site, action, effect))
1616 }
1617 _ => None,
1618 })
1619 .collect()
1620 }
1621 fn refused(&self) -> Vec<(Site, ActionKind, Refusal)> {
1622 self.events()
1623 .into_iter()
1624 .filter_map(|e| match e {
1625 ProxyEvent::ActionRefused { site, action, refusal, .. } => {
1626 Some((site, action, refusal))
1627 }
1628 _ => None,
1629 })
1630 .collect()
1631 }
1632 fn impairments(&self) -> Vec<ImpairmentKind> {
1633 self.events()
1634 .into_iter()
1635 .filter_map(|e| match e {
1636 ProxyEvent::Impairment { kind, .. } => Some(kind),
1637 _ => None,
1638 })
1639 .collect()
1640 }
1641
1642 /// Every impairment as the pair a reader actually needs — which
1643 /// connection it is about, and what it says — in emission order.
1644 ///
1645 /// A `Vec` rather than a set, and paired rather than two lists,
1646 /// because both halves of the claim are ordering claims: an
1647 /// impairment reports something that has already happened, so the
1648 /// order they arrive in is the order the proxy did things in, and a
1649 /// kind separated from its leg is a number without a label.
1650 fn attributed_impairments(&self) -> Vec<(Option<Leg>, ImpairmentKind)> {
1651 self.events()
1652 .into_iter()
1653 .filter_map(|e| match e {
1654 ProxyEvent::Impairment { leg, kind, .. } => Some((leg, kind)),
1655 _ => None,
1656 })
1657 .collect()
1658 }
1659 }
1660
1661 /// Everything a call needs, owned, so a test is four lines.
1662 struct Harness {
1663 observer: Arc<Recording>,
1664 counters: Arc<Recorder>,
1665 pending: PendingQueue,
1666 deferred: DeferredEffects,
1667 closer: SessionCloser,
1668 cancel: CancellationToken,
1669 /// The side the pipe this harness stands in for reads from.
1670 ///
1671 /// `ClientToProxy` unless a test says otherwise, which is the shape
1672 /// every test here had before the leg attribution existed. It is a
1673 /// field rather than a per-call argument because a real pipe fixes
1674 /// its side once, at the top of the forwarding task, and a test that
1675 /// could vary it per call would be able to build an event sequence
1676 /// no session can produce.
1677 side: ProxySide,
1678 }
1679
1680 impl Harness {
1681 fn new() -> Self {
1682 Self::with_config(EgressConfig::default())
1683 }
1684
1685 fn with_config(config: EgressConfig) -> Self {
1686 let counters = Arc::new(Recorder::new());
1687 let cancel = CancellationToken::new();
1688 Self {
1689 observer: Arc::new(Recording::default()),
1690 pending: PendingQueue::new(config, counters.clone()),
1691 deferred: DeferredEffects::new(),
1692 closer: SessionCloser::new(cancel.clone()),
1693 counters,
1694 cancel,
1695 side: ProxySide::ClientToProxy,
1696 }
1697 }
1698
1699 /// The same harness, standing in for the pipe that reads from the
1700 /// relay instead of the one that reads from the client.
1701 fn reading_from(mut self, side: ProxySide) -> Self {
1702 self.side = side;
1703 self
1704 }
1705
1706 fn report(&self) -> Reporter<'_> {
1707 Reporter::new(
1708 self.observer.as_ref(),
1709 true,
1710 self.counters.as_ref(),
1711 SessionId(1),
1712 self.side,
1713 Some(4),
1714 )
1715 }
1716
1717 /// A reporter with no stream, as the datagram path builds one.
1718 fn datagram_report(&self) -> Reporter<'_> {
1719 Reporter::new(
1720 self.observer.as_ref(),
1721 true,
1722 self.counters.as_ref(),
1723 SessionId(1),
1724 self.side,
1725 None,
1726 )
1727 }
1728
1729 /// The datagram site's shape: no queue at all.
1730 fn datagram_engine(&self) -> Engine<'_> {
1731 Engine { queue: None, closer: &self.closer }
1732 }
1733
1734 /// The same call on a session **nobody attached an observer to**:
1735 /// `wants_events()` answered `false`, so not one event is emitted.
1736 ///
1737 /// The counters are asserted through it, which is the only way to
1738 /// tell a figure that was measured from one that agrees with its own
1739 /// event because the same `if` guarded both.
1740 fn run_unwatched(&mut self, unit: &Unit<'_>, action: Action) -> Outcome {
1741 let report = Reporter::new(
1742 self.observer.as_ref(),
1743 false,
1744 self.counters.as_ref(),
1745 SessionId(1),
1746 self.side,
1747 Some(4),
1748 );
1749 let mut engine = Engine {
1750 queue: Some(Queue { pending: &mut self.pending, deferred: &mut self.deferred }),
1751 closer: &self.closer,
1752 };
1753 execute(unit, action, &mut engine, &report)
1754 }
1755
1756 fn run(&mut self, unit: &Unit<'_>, action: Action) -> Outcome {
1757 let report = Reporter::new(
1758 self.observer.as_ref(),
1759 true,
1760 self.counters.as_ref(),
1761 SessionId(1),
1762 self.side,
1763 Some(4),
1764 );
1765 let mut engine = Engine {
1766 queue: Some(Queue { pending: &mut self.pending, deferred: &mut self.deferred }),
1767 closer: &self.closer,
1768 };
1769 execute(unit, action, &mut engine, &report)
1770 }
1771 }
1772
1773 fn meta(draft: DraftVersion) -> ObjectMeta {
1774 ObjectMeta {
1775 draft,
1776 stream_kind: DataStreamType::Subgroup,
1777 track_alias: Some(7),
1778 group_id: 1,
1779 subgroup_id: Some(0),
1780 object_id: 3,
1781 publisher_priority: Some(128),
1782 index_in_stream: 3,
1783 payload_len: 4,
1784 status: None,
1785 end_of_range: None,
1786 }
1787 }
1788
1789 /// `[0xAA; 6]` framing followed by a four-byte payload.
1790 fn object_bytes() -> Bytes {
1791 Bytes::from_static(&[0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, b'p', b'a', b'y', b'l'])
1792 }
1793
1794 fn object_unit<'a>(m: &'a ObjectMeta, at: Instant) -> Unit<'a> {
1795 Unit {
1796 target: Target::Object { meta: m, subgroup_id_mode: None, raw: object_bytes() },
1797 draft: m.draft,
1798 arrived_at: at,
1799 }
1800 }
1801
1802 fn control_unit<'a>(draft: DraftVersion, at: Instant) -> Unit<'a> {
1803 Unit {
1804 target: Target::Control { raw: Bytes::from_static(b"control-frame") },
1805 draft,
1806 arrived_at: at,
1807 }
1808 }
1809
1810 fn datagram_unit<'a>(draft: DraftVersion, header_len: Option<usize>) -> Unit<'a> {
1811 Unit {
1812 target: Target::Datagram {
1813 raw: Bytes::from_static(&[0x01, 0x02, 0x03, b'p', b'a', b'y', b'l']),
1814 header_len,
1815 is_status: false,
1816 },
1817 draft,
1818 arrived_at: Instant::now(),
1819 }
1820 }
1821
1822 fn stream_end_unit<'a>(draft: DraftVersion, is_control_stream: bool) -> Unit<'a> {
1823 Unit { target: Target::StreamEnd { is_control_stream }, draft, arrived_at: Instant::now() }
1824 }
1825
1826 /// Every draft the vocabulary names, whether or not this build compiled
1827 /// a codec for it.
1828 ///
1829 /// The axis for the assertions that do not need one: the control site,
1830 /// the stream-end and datagram sites answer for a [`DraftVersion`]
1831 /// value, not for a decoder, and they answer the same way in every
1832 /// build — so restricting *those* sweeps to the compiled set would drop
1833 /// rows for nothing. Anything that reaches the **object** or **control**
1834 /// site sweeps [`COMPILED_DRAFTS`] instead: both are behind a decoder
1835 /// this build may not carry, and on a draft it does not carry the hook
1836 /// is never invoked at either.
1837 const ALL_DRAFTS: [DraftVersion; 14] = [
1838 DraftVersion::Draft07,
1839 DraftVersion::Draft08,
1840 DraftVersion::Draft09,
1841 DraftVersion::Draft10,
1842 DraftVersion::Draft11,
1843 DraftVersion::Draft12,
1844 DraftVersion::Draft13,
1845 DraftVersion::Draft14,
1846 DraftVersion::Draft15,
1847 DraftVersion::Draft16,
1848 DraftVersion::Draft17,
1849 DraftVersion::Draft18,
1850 DraftVersion::Draft19,
1851 DraftVersion::Draft20,
1852 ];
1853
1854 /// The drafts this build actually compiled, in publication order.
1855 ///
1856 /// Each element carries its own `#[cfg]`, so the axis is the enabled set
1857 /// and not a hardcoded fourteen — the shape `tests/action_matrix.rs` and
1858 /// the test module of `framer.rs` already use. It is the **only** honest
1859 /// axis for the object site: with no decoder for a draft the framer
1860 /// never addresses its data streams, the hook is never invoked on an
1861 /// object there, and `classify` says so — `Support::Unreachable {
1862 /// refusal: StreamNotFramed { reason: DecodeError } }`, see
1863 /// [`crate::capability::draft_is_compiled`]. Sweeping the whole
1864 /// vocabulary through `execute` therefore measures that guard rather
1865 /// than this module's executor, which is what a `--features draft07`
1866 /// build used to turn twenty-seven of the tests below red.
1867 ///
1868 /// The **control** site joined it later, and for the same reason one
1869 /// decoder along: `AnyControlMessage::decode` has no arm for an
1870 /// uncompiled draft, so `ControlStreamParser::feed` refuses every frame
1871 /// and `ProxyHook::on_control_message` is never offered one.
1872 ///
1873 /// Under the default (all-drafts) build this is all fourteen and every
1874 /// object test below runs on all of them. Under `--no-default-features`
1875 /// it is empty: that build has no object site at all, so the object
1876 /// sweeps run zero times rather than asserting the framer's verdict is
1877 /// the executor's. The sites that survive there keep their own coverage
1878 /// through [`ALL_DRAFTS`].
1879 const COMPILED_DRAFTS: &[DraftVersion] = &[
1880 #[cfg(feature = "draft07")]
1881 DraftVersion::Draft07,
1882 #[cfg(feature = "draft08")]
1883 DraftVersion::Draft08,
1884 #[cfg(feature = "draft09")]
1885 DraftVersion::Draft09,
1886 #[cfg(feature = "draft10")]
1887 DraftVersion::Draft10,
1888 #[cfg(feature = "draft11")]
1889 DraftVersion::Draft11,
1890 #[cfg(feature = "draft12")]
1891 DraftVersion::Draft12,
1892 #[cfg(feature = "draft13")]
1893 DraftVersion::Draft13,
1894 #[cfg(feature = "draft14")]
1895 DraftVersion::Draft14,
1896 #[cfg(feature = "draft15")]
1897 DraftVersion::Draft15,
1898 #[cfg(feature = "draft16")]
1899 DraftVersion::Draft16,
1900 #[cfg(feature = "draft17")]
1901 DraftVersion::Draft17,
1902 #[cfg(feature = "draft18")]
1903 DraftVersion::Draft18,
1904 #[cfg(feature = "draft19")]
1905 DraftVersion::Draft19,
1906 #[cfg(feature = "draft20")]
1907 DraftVersion::Draft20,
1908 ];
1909
1910 // ── how many events one action produces ─────────────────────────
1911
1912 #[test]
1913 fn an_applied_action_emits_exactly_one_event() {
1914 for &draft in COMPILED_DRAFTS {
1915 let mut h = Harness::new();
1916 let m = meta(draft);
1917 let out = h.run(&object_unit(&m, Instant::now()), Action::Pass);
1918 assert_eq!(out.plan, Plan::WriteNow(object_bytes()), "draft {draft:?}");
1919 assert_eq!(out.result, Ok(Effect::ForwardedVerbatim), "draft {draft:?}");
1920 assert_eq!(h.observer.events().len(), 1, "draft {draft:?}");
1921 assert_eq!(
1922 h.observer.applied(),
1923 vec![(Site::Object, ActionKind::Pass, Effect::ForwardedVerbatim)],
1924 "draft {draft:?}",
1925 );
1926 assert_eq!(h.counters.snapshot().actions_refused, 0, "draft {draft:?}");
1927 }
1928 }
1929
1930 #[test]
1931 fn a_refusal_emits_exactly_one_event_and_bumps_exactly_one_counter() {
1932 for &draft in COMPILED_DRAFTS {
1933 let mut h = Harness::new();
1934 let m = meta(draft);
1935 let out = h.run(
1936 &object_unit(&m, Instant::now()),
1937 Action::Replace(Bytes::from_static(b"nope")),
1938 );
1939 assert_eq!(h.observer.events().len(), 1, "draft {draft:?}");
1940 assert_eq!(h.counters.snapshot().actions_refused, 1, "draft {draft:?}");
1941 // The unit is forwarded unchanged.
1942 assert_eq!(out.plan, Plan::WriteNow(object_bytes()), "draft {draft:?}");
1943 }
1944 }
1945
1946 #[test]
1947 fn the_counter_moves_even_with_no_observer_attached() {
1948 for &draft in COMPILED_DRAFTS {
1949 let counters = Arc::new(Recorder::new());
1950 let observer = Recording::default();
1951 let cancel = CancellationToken::new();
1952 let closer = SessionCloser::new(cancel);
1953 let mut pending = PendingQueue::new(EgressConfig::default(), counters.clone());
1954 let mut deferred = DeferredEffects::new();
1955 let report = Reporter::new(
1956 &observer,
1957 false, // observer.wants_events() == false
1958 counters.as_ref(),
1959 SessionId(1),
1960 ProxySide::ClientToProxy,
1961 Some(1),
1962 );
1963 let mut engine = Engine {
1964 queue: Some(Queue { pending: &mut pending, deferred: &mut deferred }),
1965 closer: &closer,
1966 };
1967 let m = meta(draft);
1968 let out = execute(
1969 &object_unit(&m, Instant::now()),
1970 Action::Replace(Bytes::from_static(b"x")),
1971 &mut engine,
1972 &report,
1973 );
1974 // `Replace` at the object site is `WrongSite { .. ReplaceObject }`
1975 // on every draft — the executor's refusal, not the framer's.
1976 assert_eq!(
1977 out.result,
1978 Err(Refusal::WrongSite { site: Site::Object, action: ActionKind::ReplaceObject }),
1979 "draft {draft:?}",
1980 );
1981 assert!(observer.events().is_empty(), "events are gated");
1982 assert_eq!(counters.snapshot().actions_refused, 1, "counters are not");
1983 }
1984 }
1985
1986 /// The module note's cardinality contract, swept rather than argued:
1987 /// **exactly one** of `ActionApplied` / `ActionRefused` per `execute`,
1988 /// never both and never neither, and a refused unit's plan is its own
1989 /// bytes, unchanged.
1990 ///
1991 /// `every_refusal_this_module_emits_is_classifys_or_one_of_its_own_three`
1992 /// sweeps *which* refusals may appear; this sweeps *how many events*
1993 /// they arrive with, which is the half a partially-applied action would
1994 /// break. Impairments are excluded on purpose — a clamp or a
1995 /// backpressure transition is not a decision.
1996 ///
1997 /// *Ablation:* in `execute`'s `Err` arm, call
1998 /// `report.applied(site, action, Effect::ForwardedVerbatim)` beside
1999 /// `report.refused(..)`. Every refusing cell fails the one-decision
2000 /// assertion.
2001 #[test]
2002 fn exactly_one_decision_event_per_unit_and_refusals_forward_the_original() {
2003 let mut refused_cells = 0usize;
2004 let mut applied_cells = 0usize;
2005 for draft in ALL_DRAFTS {
2006 let m = meta(draft);
2007 // The control, stream-end and datagram cells answer for every
2008 // draft in the vocabulary; the object cell only for one this
2009 // build compiled (`COMPILED_DRAFTS`), because on the rest the
2010 // hook is never invoked there at all.
2011 let object_site_is_reachable = COMPILED_DRAFTS.contains(&draft);
2012 let actions = || {
2013 vec![
2014 Action::Pass,
2015 Action::Replace(Bytes::from_static(b"xxxx")),
2016 Action::ReplacePayload(Bytes::from_static(b"abcd")),
2017 Action::ReplacePayload(Bytes::from_static(b"toolong")),
2018 Action::Drop(DropMode::Elide),
2019 Action::Truncate { bytes: 2, code: 1 },
2020 Action::ResetStream { code: u64::MAX },
2021 Action::CloseSession { code: 1, reason: Bytes::new() },
2022 Action::Pass.delayed(Duration::from_millis(1)),
2023 Action::Pass.held(Gate::new()),
2024 Action::Drop(DropMode::Elide).delayed(Duration::from_millis(1)),
2025 Action::Drop(DropMode::Elide).held(Gate::new()),
2026 Action::ResetStream { code: 1 }.delayed(Duration::from_millis(1)),
2027 Action::CloseSession { code: 1, reason: Bytes::new() }.held(Gate::new()),
2028 ]
2029 };
2030 for action in actions() {
2031 // Each cell gets its own harness, so the queue is empty and
2032 // a refusal's forward is the inline write, not a slot.
2033 let mut cells: Vec<(Unit<'_>, Option<Bytes>)> = vec![
2034 (
2035 control_unit(draft, Instant::now()),
2036 Some(Bytes::from_static(b"control-frame")),
2037 ),
2038 (stream_end_unit(draft, false), None),
2039 (stream_end_unit(draft, true), None),
2040 ];
2041 if object_site_is_reachable {
2042 cells.push((object_unit(&m, Instant::now()), Some(object_bytes())));
2043 }
2044 for (unit, original) in cells {
2045 let mut h = Harness::new();
2046 let out = h.run(&unit, action.clone());
2047 let what = format!("{draft:?} / {:?} / {action:?}", unit.target.site());
2048 let decisions = h
2049 .observer
2050 .events()
2051 .iter()
2052 .filter(|e| {
2053 matches!(
2054 e,
2055 ProxyEvent::ActionApplied { .. } | ProxyEvent::ActionRefused { .. }
2056 )
2057 })
2058 .count();
2059 assert_eq!(decisions, 1, "[{what}] one decision event per unit");
2060 match &out.result {
2061 Ok(_) => {
2062 applied_cells += 1;
2063 assert!(h.observer.refused().is_empty(), "[{what}]");
2064 assert_eq!(h.counters.snapshot().actions_refused, 0, "[{what}]");
2065 }
2066 Err(_) => {
2067 refused_cells += 1;
2068 assert!(
2069 h.observer.applied().is_empty(),
2070 "[{what}] a refused unit reports no ActionApplied",
2071 );
2072 assert_eq!(h.counters.snapshot().actions_refused, 1, "[{what}]");
2073 let want = match &original {
2074 Some(raw) => Plan::WriteNow(raw.clone()),
2075 None => Plan::Nothing,
2076 };
2077 assert_eq!(
2078 out.plan, want,
2079 "[{what}] a refused unit is forwarded unchanged",
2080 );
2081 assert!(!out.note_elided, "[{what}]");
2082 assert!(out.clamped.is_none(), "[{what}]");
2083 assert!(
2084 h.pending.is_empty(),
2085 "[{what}] a refusal on an empty queue writes inline",
2086 );
2087 assert_eq!(h.counters.snapshot().egress_items_queued, 0, "[{what}]");
2088 }
2089 }
2090 }
2091
2092 // The datagram site takes the same entry point with no queue.
2093 let h = Harness::new();
2094 let unit = datagram_unit(draft, Some(3));
2095 let raw = Bytes::from_static(&[0x01, 0x02, 0x03, b'p', b'a', b'y', b'l']);
2096 let report = h.datagram_report();
2097 let mut engine = h.datagram_engine();
2098 let out = execute(&unit, action.clone(), &mut engine, &report);
2099 let what = format!("{draft:?} / Datagram / {action:?}");
2100 let decisions = h
2101 .observer
2102 .events()
2103 .iter()
2104 .filter(|e| {
2105 matches!(
2106 e,
2107 ProxyEvent::ActionApplied { .. } | ProxyEvent::ActionRefused { .. }
2108 )
2109 })
2110 .count();
2111 assert_eq!(decisions, 1, "[{what}] one decision event per unit");
2112 if out.result.is_err() {
2113 refused_cells += 1;
2114 assert!(h.observer.applied().is_empty(), "[{what}]");
2115 assert_eq!(out.plan, Plan::WriteNow(raw), "[{what}] forwarded unchanged");
2116 } else {
2117 applied_cells += 1;
2118 assert!(h.observer.refused().is_empty(), "[{what}]");
2119 }
2120 }
2121 }
2122 assert!(refused_cells > 0 && applied_cells > 0, "the sweep must reach both verdicts");
2123 }
2124
2125 /// A refusal behind a delayed unit takes an ordering slot rather than
2126 /// overtaking it — `execute`'s "through the same queue-or-write-now
2127 /// fork an admitted `Pass` takes" clause, which the empty-queue sweep
2128 /// above cannot reach.
2129 ///
2130 /// *Ablation:* make `forward_unchanged` return `Plan::WriteNow(raw)`
2131 /// unconditionally. The refused object then jumps the delayed one and
2132 /// `pending.len()` stays at 1.
2133 #[test]
2134 fn a_refused_unit_queues_behind_a_delayed_one_rather_than_overtaking_it() {
2135 const DELAY: Duration = Duration::from_millis(60);
2136 for &draft in COMPILED_DRAFTS {
2137 let mut h = Harness::new();
2138 let m = meta(draft);
2139 let at = Instant::now();
2140 h.run(&object_unit(&m, at), Action::Pass.delayed(DELAY));
2141 // `Replace` at the object site is `WrongSite { .. ReplaceObject }`.
2142 let out = h.run(&object_unit(&m, at), Action::Replace(Bytes::from_static(b"no")));
2143 assert!(out.result.is_err(), "draft {draft:?}");
2144 assert_eq!(out.plan, Plan::Nothing, "not written inline: the queue is busy");
2145 assert_eq!(h.pending.len(), 2, "draft {draft:?}");
2146 assert_eq!(h.deferred.len(), 2, "one ledger entry per pushed unit, refusals included");
2147
2148 assert!(
2149 h.pending.pop_next_due(Instant::now()).is_none(),
2150 "the delayed head still blocks the refused unit behind it",
2151 );
2152 let far = Instant::now() + Duration::from_secs(3600);
2153 let head = h.pending.pop_next_due(far).expect("the delayed Pass");
2154 let behind = h.pending.pop_next_due(far).expect("the refused unit, behind it");
2155 assert_eq!(*head.item(), Item::Write(object_bytes()));
2156 assert_eq!(
2157 *behind.item(),
2158 Item::Write(object_bytes()),
2159 "the refused unit's own bytes, not the replacement",
2160 );
2161 assert!(behind.expected_at() >= head.expected_at(), "and not expected ahead of it");
2162 assert_eq!(
2163 h.deferred.take_all(),
2164 vec![Deferred { action: ActionKind::Pass, effect: Effect::ForwardedVerbatim }],
2165 "the refused unit owes no release event",
2166 );
2167 }
2168 }
2169
2170 // ── the ruling: Delay { then: Replace } is two events ────────────
2171
2172 /// The ruling this module was asked to pin, at the one site where
2173 /// `Replace` is a legal inner action — the control site. (`Replace` at
2174 /// the object site is `WrongSite { .. ReplaceObject }` on every draft,
2175 /// so `Delay { then: Replace }` there is a *refusal*, which
2176 /// `a_delay_wrapping_a_refused_inner_action_is_refused` covers.)
2177 #[test]
2178 fn delay_then_replace_reports_queued_now_and_the_inner_effect_at_release() {
2179 let Some(draft) = a_compiled_draft() else { return };
2180 let mut h = Harness::new();
2181 let at = Instant::now();
2182 let out = h.run(
2183 &control_unit(draft, at),
2184 Action::Replace(Bytes::from_static(b"1234")).delayed(Duration::from_millis(50)),
2185 );
2186
2187 // Step 1, at the decision.
2188 assert_eq!(out.plan, Plan::Nothing);
2189 let Ok(Effect::Queued { release_at }) = out.result else {
2190 panic!("expected Queued, got {:?}", out.result)
2191 };
2192 assert!(release_at >= at + Duration::from_millis(50));
2193 assert_eq!(h.observer.applied().len(), 1);
2194 assert_eq!(h.observer.applied()[0].1, ActionKind::Delay);
2195
2196 // Step 2, at the release. One ledger entry, naming the inner kind.
2197 assert_eq!(h.deferred.len(), 1);
2198 assert_eq!(h.pending.len(), 1);
2199 let owed = h.deferred.pop().expect("the delay owes a release event");
2200 assert_eq!(
2201 owed,
2202 Deferred { action: ActionKind::Replace, effect: Effect::Replaced { bytes: 4 } },
2203 );
2204 h.report().applied_deferred(Site::Control, owed);
2205
2206 let applied = h.observer.applied();
2207 assert_eq!(applied.len(), 2, "a deferred action reports twice");
2208 assert_eq!(applied[1].1, ActionKind::Replace);
2209 assert_eq!(applied[1].2, Effect::Replaced { bytes: 4 });
2210 }
2211
2212 #[test]
2213 fn a_delay_wrapping_a_refused_inner_action_is_refused_with_the_inners_reason() {
2214 for &draft in COMPILED_DRAFTS {
2215 let mut h = Harness::new();
2216 let m = meta(draft);
2217 let out = h.run(
2218 &object_unit(&m, Instant::now()),
2219 Action::Replace(Bytes::from_static(b"1234")).delayed(Duration::from_millis(50)),
2220 );
2221 assert_eq!(
2222 out.result,
2223 Err(Refusal::WrongSite { site: Site::Object, action: ActionKind::ReplaceObject }),
2224 "draft {draft:?}",
2225 );
2226 assert_eq!(
2227 h.observer.refused()[0].1,
2228 ActionKind::Replace,
2229 "the event names the inner action, which is the informative one",
2230 );
2231 assert!(h.pending.is_empty(), "the site check happens before the queue");
2232 }
2233 }
2234
2235 #[test]
2236 fn a_direct_action_queued_only_for_ordering_owes_nothing_at_release() {
2237 for &draft in COMPILED_DRAFTS {
2238 let mut h = Harness::new();
2239 let m = meta(draft);
2240 let at = Instant::now();
2241 // Head of the queue: delayed, so the queue is busy.
2242 h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_millis(80)));
2243 // A plain `Pass` behind it must not overtake it — and must report
2244 // once, now, not twice.
2245 let out = h.run(&object_unit(&m, at), Action::Pass);
2246 assert_eq!(out.plan, Plan::Nothing, "a busy queue swallows the write");
2247 assert_eq!(out.result, Ok(Effect::ForwardedVerbatim), "draft {draft:?}");
2248 assert_eq!(h.pending.len(), 2, "draft {draft:?}");
2249 assert_eq!(h.deferred.len(), 2, "one ledger entry per pushed unit");
2250
2251 assert!(h.deferred.pop().is_some(), "the delayed unit owes one");
2252 assert!(h.deferred.pop().is_none(), "the ordering-only unit owes none");
2253 assert_eq!(h.observer.applied().len(), 2, "two decisions, two events");
2254 }
2255 }
2256
2257 #[test]
2258 fn hold_reports_queued_at_the_ceiling_and_owes_the_inner_effect() {
2259 for &draft in COMPILED_DRAFTS {
2260 let mut h = Harness::new();
2261 let m = meta(draft);
2262 let at = Instant::now();
2263 let gate = Gate::new();
2264 let out = h.run(&object_unit(&m, at), Action::Pass.held(gate.clone()));
2265 let Ok(Effect::Queued { release_at }) = out.result else {
2266 panic!("[{draft:?}] expected Queued, got {:?}", out.result)
2267 };
2268 assert!(release_at >= at + Duration::from_secs(30) - Duration::from_millis(1));
2269 assert_eq!(out.clamped, None, "a Hold has no requested duration to clamp");
2270 assert_eq!(
2271 h.deferred.pop(),
2272 Some(Deferred { action: ActionKind::Pass, effect: Effect::ForwardedVerbatim }),
2273 );
2274 assert!(!gate.is_released());
2275 }
2276 }
2277
2278 // ── composition ─────────────────────────────────────────────────
2279
2280 #[test]
2281 fn a_delay_wrapping_a_terminal_is_refused_before_it_is_queued() {
2282 for &draft in COMPILED_DRAFTS {
2283 let mut h = Harness::new();
2284 let m = meta(draft);
2285 let out = h.run(
2286 &object_unit(&m, Instant::now()),
2287 Action::ResetStream { code: 2 }.delayed(Duration::from_millis(10)),
2288 );
2289 assert_eq!(out.result, Err(WRAPPED_TERMINAL), "draft {draft:?}");
2290 assert_eq!(
2291 h.observer.refused(),
2292 vec![(Site::Object, ActionKind::ResetStream, WRAPPED_TERMINAL)],
2293 );
2294 assert!(h.pending.is_empty(), "nothing reached the deque");
2295 assert!(h.deferred.is_empty());
2296 assert_eq!(h.counters.snapshot().egress_items_queued, 0);
2297 // Refused units are still forwarded, unchanged.
2298 assert_eq!(out.plan, Plan::WriteNow(object_bytes()));
2299 }
2300 }
2301
2302 #[test]
2303 fn every_illegal_composition_names_which_one_it_was() {
2304 let cases = [
2305 (Action::Pass.delayed(Duration::from_millis(1)), NESTED_MODIFIER),
2306 (Action::Pass.held(Gate::new()), NESTED_MODIFIER),
2307 (Action::Truncate { bytes: 1, code: 0 }, WRAPPED_TERMINAL),
2308 (Action::ResetStream { code: 0 }, WRAPPED_TERMINAL),
2309 (Action::CloseSession { code: 1, reason: Bytes::new() }, WRAPPED_CLOSE),
2310 ];
2311 for &draft in COMPILED_DRAFTS {
2312 for (inner, expected) in cases.clone() {
2313 let mut h = Harness::new();
2314 let m = meta(draft);
2315 let out = h.run(
2316 &object_unit(&m, Instant::now()),
2317 inner.clone().delayed(Duration::from_millis(1)),
2318 );
2319 assert_eq!(out.result, Err(expected.clone()), "draft {draft:?} / inner {inner:?}");
2320 assert!(h.pending.is_empty());
2321 }
2322 }
2323 }
2324
2325 /// The list of legal inner actions, both directions, against the one
2326 /// function that decides it.
2327 ///
2328 /// A refusal on *other* grounds is not what this asserts — a wrapped
2329 /// `ReplacePayload` at the control site is `WrongSite`, and rightly.
2330 /// What must never happen is `WrongComposition` naming one of the four
2331 /// content actions.
2332 ///
2333 /// *Ablation:* move `Action::Drop(_)` out of `check_composition`'s `Ok`
2334 /// arm into any of the three refusing arms. The first loop fails on
2335 /// `Drop(Elide)`.
2336 #[test]
2337 fn the_composition_rule_admits_exactly_the_four_content_actions() {
2338 for inner in [
2339 Action::Pass,
2340 Action::Replace(Bytes::from_static(b"xxxx")),
2341 Action::ReplacePayload(Bytes::from_static(b"abcd")),
2342 Action::Drop(DropMode::Elide),
2343 ] {
2344 assert!(
2345 check_composition(&inner).is_ok(),
2346 "{inner:?} is a content action and must be legal inside Delay/Hold",
2347 );
2348 }
2349 for (inner, expected) in [
2350 (Action::Pass.delayed(Duration::ZERO), NESTED_MODIFIER),
2351 (Action::Pass.held(Gate::new()), NESTED_MODIFIER),
2352 (Action::Truncate { bytes: 1, code: 0 }, WRAPPED_TERMINAL),
2353 (Action::ResetStream { code: 0 }, WRAPPED_TERMINAL),
2354 (Action::CloseSession { code: 0, reason: Bytes::new() }, WRAPPED_CLOSE),
2355 ] {
2356 let Err(Refused { action, refusal }) = check_composition(&inner) else {
2357 panic!("{inner:?} is not a content action and must be refused")
2358 };
2359 assert_eq!(refusal, expected, "inner {inner:?}");
2360 assert_eq!(action, kind_of(&inner), "the event names what was wrapped");
2361 }
2362 }
2363
2364 /// The composition ruling that the `Action` rustdoc used to state
2365 /// backwards, and the measurement behind it.
2366 ///
2367 /// `Drop` is one of the four legal inner actions, and so says the
2368 /// sentence directly above the one that used to call
2369 /// `Delay { then: Drop(_) }` "unobservable" and name it as refused. It
2370 /// is admitted, and it is not unobservable: the drop takes an ordering
2371 /// slot for the whole of its delay, so the undelayed `Pass` pushed
2372 /// behind it is clamped to the drop's release instead of going out
2373 /// inline. Deleting the unit and stalling the stream behind it is one
2374 /// impairment with two effects, both on the wire.
2375 ///
2376 /// *Ablation, both ways:*
2377 /// * refuse `Drop` in `check_composition` — the first assertion fails
2378 /// with `Err(WrongComposition { .. })`, which is the defect this test
2379 /// was written for;
2380 /// * keep the admission but let a `Payload::Elide` skip `push_unit` in
2381 /// the `Delay` arm — the drop then holds no slot, the `Pass` behind it
2382 /// comes back `Plan::WriteNow` and overtakes an object the hook had
2383 /// already ordered ahead of it.
2384 #[test]
2385 fn a_delayed_drop_is_admitted_and_blocks_the_stream_behind_it() {
2386 const DELAY: Duration = Duration::from_millis(80);
2387 for &draft in COMPILED_DRAFTS {
2388 let mut h = Harness::new();
2389 let m = meta(draft);
2390 let at = Instant::now();
2391 // Whether the elide leaves a successor to renumber is the delta
2392 // drafts' business, not this test's; the table itself is pinned
2393 // by `elide_renumbering_names_the_delta_encoding_drafts`.
2394 let renumbered = elide_renumbers_successor(&object_unit(&m, at));
2395
2396 let out = h.run(&object_unit(&m, at), Action::Drop(DropMode::Elide).delayed(DELAY));
2397 let Ok(Effect::Queued { release_at }) = out.result else {
2398 panic!("[{draft:?}] a delayed Drop is admitted, not refused; got {:?}", out.result)
2399 };
2400 assert!(release_at >= at + DELAY);
2401 assert!(out.note_elided, "the framer's cursor still moves at the decision");
2402 assert_eq!(h.counters.snapshot().actions_refused, 0);
2403 assert!(h.observer.refused().is_empty(), "nothing was refused");
2404 assert_eq!(
2405 h.observer.applied(),
2406 vec![(Site::Object, ActionKind::Delay, Effect::Queued { release_at })],
2407 "step 1 names the modifier",
2408 );
2409
2410 // The observability claim: the next object cannot overtake it.
2411 let behind = h.run(&object_unit(&m, at), Action::Pass);
2412 assert_eq!(behind.plan, Plan::Nothing, "the drop's slot is still holding the queue");
2413 assert_eq!(behind.result, Ok(Effect::ForwardedVerbatim));
2414 assert_eq!(h.deferred.len(), h.pending.len());
2415 assert_eq!(
2416 h.deferred.take_all(),
2417 vec![Deferred {
2418 action: ActionKind::DropElide,
2419 effect: Effect::Elided { renumbered_successor: renumbered },
2420 }],
2421 "step 2 names the inner action; the ordering-only Pass owes nothing",
2422 );
2423
2424 // The `Pass` is due on its own account the instant it is pushed,
2425 // and is still not writable: the elided drop is in front of it
2426 // and is not due. That is the head-of-line block, stated as
2427 // behaviour rather than as arithmetic.
2428 assert!(
2429 h.pending.pop_next_due(Instant::now()).is_none(),
2430 "a delayed drop head-of-line-blocks the undelayed unit behind it",
2431 );
2432
2433 let far = Instant::now() + Duration::from_secs(3600);
2434 let dropped = h.pending.pop_next_due(far).expect("the drop holds a slot");
2435 let passed = h.pending.pop_next_due(far).expect("the Pass is queued behind it");
2436 assert_eq!(*dropped.item(), Item::Elided, "the drop writes nothing...");
2437 assert!(dropped.due_at() >= at + DELAY, "...and not until its delay is up");
2438 assert_eq!(*passed.item(), Item::Write(object_bytes()));
2439 assert!(
2440 passed.expected_at() >= at + DELAY,
2441 "the queue expects to write the unit behind the drop no earlier than the drop: \
2442 {:?} is earlier than {:?}",
2443 passed.expected_at(),
2444 at + DELAY,
2445 );
2446 }
2447 }
2448
2449 /// The same ruling under a [`Gate`]. The gate is the observable here:
2450 /// the unit is not due while the gate holds, and is the moment it is
2451 /// released.
2452 ///
2453 /// *Ablation:* refuse `Drop` in `check_composition`; the first
2454 /// assertion fails.
2455 #[test]
2456 fn a_held_drop_is_admitted_and_stays_undue_until_its_gate_is_released() {
2457 for &draft in COMPILED_DRAFTS {
2458 let mut h = Harness::new();
2459 let m = meta(draft);
2460 let at = Instant::now();
2461 let renumbered = elide_renumbers_successor(&object_unit(&m, at));
2462 let gate = Gate::new();
2463 let out = h.run(&object_unit(&m, at), Action::Drop(DropMode::Elide).held(gate.clone()));
2464 assert!(
2465 matches!(out.result, Ok(Effect::Queued { .. })),
2466 "[{draft:?}] a held Drop is admitted, not refused; got {:?}",
2467 out.result,
2468 );
2469 assert_eq!(h.counters.snapshot().actions_refused, 0);
2470 assert!(
2471 h.pending.pop_next_due(Instant::now()).is_none(),
2472 "nothing is due while the gate holds",
2473 );
2474 gate.release();
2475 let released =
2476 h.pending.pop_next_due(Instant::now()).expect("a released gate makes it due");
2477 assert_eq!(*released.item(), Item::Elided);
2478 assert_eq!(
2479 h.deferred.take_all(),
2480 vec![Deferred {
2481 action: ActionKind::DropElide,
2482 effect: Effect::Elided { renumbered_successor: renumbered },
2483 }],
2484 );
2485 }
2486 }
2487
2488 #[test]
2489 fn the_site_verdict_wins_over_the_composition_verdict() {
2490 // `Delay` is refused at the datagram site. A bad composition
2491 // inside it must not shadow that site verdict.
2492 let h = Harness::new();
2493 let unit = datagram_unit(DraftVersion::Draft11, Some(3));
2494 let report = h.datagram_report();
2495 let mut engine = h.datagram_engine();
2496 let out = execute(
2497 &unit,
2498 Action::ResetStream { code: 1 }.delayed(Duration::from_millis(5)),
2499 &mut engine,
2500 &report,
2501 );
2502 assert_eq!(
2503 out.result,
2504 Err(Refusal::WrongSite { site: Site::Datagram, action: ActionKind::Delay }),
2505 );
2506 }
2507
2508 // ── refusal propagation, verbatim from `classify` ────────────────
2509
2510 #[test]
2511 fn replace_at_the_object_site_propagates_classifys_replaceobject_refusal() {
2512 for &draft in COMPILED_DRAFTS {
2513 let mut h = Harness::new();
2514 let m = meta(draft);
2515 let out =
2516 h.run(&object_unit(&m, Instant::now()), Action::Replace(Bytes::from_static(b"x")));
2517 // The refusal is the table's — `ReplaceObject`, not `Replace`.
2518 assert_eq!(
2519 out.result,
2520 Err(Refusal::WrongSite { site: Site::Object, action: ActionKind::ReplaceObject }),
2521 "draft {draft:?}",
2522 );
2523 // The event's `action` is what the hook returned.
2524 assert_eq!(h.observer.refused()[0].1, ActionKind::Replace);
2525 // And it is byte-for-byte what the published table says.
2526 let published = crate::capability::Capabilities::for_draft(draft)
2527 .supports(Site::Object, ActionKind::ReplaceObject);
2528 assert_eq!(published, Support::No(out.result.unwrap_err()), "draft {draft:?}");
2529 }
2530 }
2531
2532 #[test]
2533 fn every_refusal_this_module_emits_is_classifys_or_one_of_its_own_three() {
2534 // The exhaustive statement of the module note: sweep a wide set of
2535 // (site, action) pairs and assert every refusal that comes back is
2536 // either one `classify` produced for the same pair, or one of the
2537 // three the executor owns.
2538 let mut seen: Vec<Refusal> = Vec::new();
2539 for draft in ALL_DRAFTS {
2540 let m = meta(draft);
2541 let actions = || {
2542 vec![
2543 Action::Pass,
2544 Action::Replace(Bytes::from_static(b"xxxx")),
2545 Action::ReplacePayload(Bytes::from_static(b"abcd")),
2546 Action::ReplacePayload(Bytes::from_static(b"toolong")),
2547 Action::Drop(DropMode::Elide),
2548 Action::Truncate { bytes: 2, code: 1 },
2549 Action::Truncate { bytes: 2, code: u64::MAX },
2550 Action::ResetStream { code: 1 },
2551 Action::ResetStream { code: u64::MAX },
2552 Action::CloseSession { code: 1, reason: Bytes::new() },
2553 Action::Pass.delayed(Duration::from_millis(1)),
2554 Action::Pass.held(Gate::new()),
2555 Action::ResetStream { code: 1 }.delayed(Duration::from_millis(1)),
2556 ]
2557 };
2558 for action in actions() {
2559 // As above: the object cell exists only where a decoder
2560 // does, so that the sweep collects the executor's refusals
2561 // and not the framer's `StreamNotFramed` on a draft this
2562 // build cannot address.
2563 let mut units = vec![
2564 control_unit(draft, Instant::now()),
2565 stream_end_unit(draft, false),
2566 stream_end_unit(draft, true),
2567 ];
2568 if COMPILED_DRAFTS.contains(&draft) {
2569 units.push(object_unit(&m, Instant::now()));
2570 }
2571 for unit in units {
2572 let mut h = Harness::new();
2573 let out = h.run(&unit, action.clone());
2574 if let Err(refusal) = out.result {
2575 assert_eq!(h.counters.snapshot().actions_refused, 1);
2576 seen.push(refusal);
2577 } else {
2578 assert_eq!(h.counters.snapshot().actions_refused, 0);
2579 }
2580 }
2581 let h = Harness::new();
2582 let unit = datagram_unit(draft, Some(3));
2583 let report = h.datagram_report();
2584 let mut engine = h.datagram_engine();
2585 let out = execute(&unit, action.clone(), &mut engine, &report);
2586 if let Err(refusal) = out.result {
2587 seen.push(refusal);
2588 }
2589 }
2590 }
2591 assert!(!seen.is_empty(), "the sweep must actually refuse things");
2592 for refusal in &seen {
2593 assert!(
2594 !matches!(refusal, Refusal::StreamNotFramed { .. }),
2595 "table-only refusal {refusal:?} escaped into an ActionRefused",
2596 );
2597 }
2598 // All three executor-owned refusals are reachable from the sweep.
2599 assert!(seen.iter().any(|r| matches!(r, Refusal::WrongComposition { .. })));
2600 assert!(seen.iter().any(|r| matches!(r, Refusal::ErrorCodeOutOfRange { .. })));
2601 }
2602
2603 #[test]
2604 fn a_second_close_is_refused_as_session_already_closing() {
2605 for &draft in COMPILED_DRAFTS {
2606 let mut h = Harness::new();
2607 let m = meta(draft);
2608 let first = h.run(
2609 &object_unit(&m, Instant::now()),
2610 Action::CloseSession { code: 3, reason: Bytes::from_static(b"bye") },
2611 );
2612 assert_eq!(first.result, Ok(Effect::SessionClosing { code: 3 }), "draft {draft:?}");
2613 assert_eq!(
2614 first.plan,
2615 Plan::CloseSession { code: 3, reason: Bytes::from_static(b"bye") },
2616 );
2617 let second = h.run(
2618 &object_unit(&m, Instant::now()),
2619 Action::CloseSession { code: 1, reason: Bytes::new() },
2620 );
2621 assert_eq!(second.result, Err(Refusal::SessionAlreadyClosing), "draft {draft:?}");
2622 assert_eq!(
2623 h.closer.close_args(),
2624 (3, Bytes::from_static(b"bye")),
2625 "the first request wins; the second does not overwrite the reason",
2626 );
2627 assert!(h.cancel.is_cancelled(), "SessionCloser::request cancels as it records");
2628 // The refused second close still forwarded its unit unchanged.
2629 assert_eq!(second.plan, Plan::WriteNow(object_bytes()));
2630 }
2631 }
2632
2633 #[test]
2634 fn an_out_of_range_code_is_refused_before_anything_is_queued() {
2635 for &draft in COMPILED_DRAFTS {
2636 for action in [
2637 Action::ResetStream { code: 1 << 62 },
2638 Action::Truncate { bytes: 1, code: u64::MAX },
2639 ] {
2640 let mut h = Harness::new();
2641 let m = meta(draft);
2642 let out = h.run(&object_unit(&m, Instant::now()), action.clone());
2643 let Err(Refusal::ErrorCodeOutOfRange { code }) = out.result else {
2644 panic!(
2645 "[{draft:?}] expected ErrorCodeOutOfRange for {action:?}, got {:?}",
2646 out.result,
2647 )
2648 };
2649 assert!(code > MAX_APPLICATION_ERROR_CODE);
2650 assert!(h.pending.is_empty(), "nothing was queued");
2651 }
2652 }
2653 // And at the stream sites, which take the other entry point.
2654 let h = Harness::new();
2655 let out = execute_stream(
2656 StreamSite::Open,
2657 DraftVersion::Draft11,
2658 StreamAction::Reject { code: u64::MAX },
2659 &h.report(),
2660 );
2661 assert_eq!(out.result, Err(Refusal::ErrorCodeOutOfRange { code: u64::MAX }));
2662 assert_eq!(out.plan, Plan::Nothing);
2663 }
2664
2665 // ── the object site ─────────────────────────────────────────────
2666
2667 #[test]
2668 fn replace_payload_splices_at_the_trailing_field() {
2669 for &draft in COMPILED_DRAFTS {
2670 let mut h = Harness::new();
2671 let m = meta(draft);
2672 let out = h.run(
2673 &object_unit(&m, Instant::now()),
2674 Action::ReplacePayload(Bytes::from_static(b"WXYZ")),
2675 );
2676 assert_eq!(out.result, Ok(Effect::Replaced { bytes: 10 }), "draft {draft:?}");
2677 let Plan::WriteNow(bytes) = out.plan else {
2678 panic!("[{draft:?}] expected an inline write")
2679 };
2680 assert_eq!(&bytes[..6], &object_bytes()[..6], "framing is untouched");
2681 assert_eq!(&bytes[6..], b"WXYZ");
2682 }
2683 }
2684
2685 #[test]
2686 fn replace_payload_with_a_different_length_is_refused_with_the_lengths() {
2687 for &draft in COMPILED_DRAFTS {
2688 let mut h = Harness::new();
2689 let m = meta(draft);
2690 let out = h.run(
2691 &object_unit(&m, Instant::now()),
2692 Action::ReplacePayload(Bytes::from_static(b"WXYZ!")),
2693 );
2694 assert_eq!(
2695 out.result,
2696 Err(Refusal::LengthChanged { from: 4, to: 5 }),
2697 "draft {draft:?}",
2698 );
2699 assert_eq!(out.plan, Plan::WriteNow(object_bytes()), "forwarded unchanged");
2700 }
2701 }
2702
2703 /// Draft-11 by name and by `#[cfg]`: the refusal exists only on the drafts
2704 /// with a *subgroup ID is the first object's ID* stream type (11-14 and
2705 /// 16-19 — not 07-10, not 15), so the fixture picks one and the test
2706 /// compiles exactly where that one was compiled. `capability::tests` owns
2707 /// the sweep over the whole set.
2708 #[cfg(feature = "draft11")]
2709 #[test]
2710 fn eliding_the_first_object_of_an_implicit_subgroup_is_refused() {
2711 let mut h = Harness::new();
2712 let mut m = meta(DraftVersion::Draft11);
2713 m.index_in_stream = 0;
2714 m.subgroup_id = None;
2715 let out = h.run(&object_unit(&m, Instant::now()), Action::Drop(DropMode::Elide));
2716 assert_eq!(out.result, Err(Refusal::WouldRedefineSubgroupId));
2717 assert!(!out.note_elided, "a refused elide must not move the cursor");
2718 assert_eq!(h.counters.snapshot().objects_elided, 0);
2719 }
2720
2721 /// The reserved mode is a draft-17-to-19 header field, and the fixture
2722 /// is a draft-19 header — so, like its neighbour, it compiles exactly
2723 /// where its draft did.
2724 #[cfg(feature = "draft19")]
2725 #[test]
2726 fn a_reserved_subgroup_id_mode_is_its_own_refusal() {
2727 let mut h = Harness::new();
2728 let mut m = meta(DraftVersion::Draft19);
2729 m.index_in_stream = 0;
2730 m.subgroup_id = None;
2731 let unit = Unit {
2732 target: Target::Object { meta: &m, subgroup_id_mode: Some(3), raw: object_bytes() },
2733 draft: DraftVersion::Draft19,
2734 arrived_at: Instant::now(),
2735 };
2736 let out = h.run(&unit, Action::Drop(DropMode::Elide));
2737 assert_eq!(out.result, Err(Refusal::ReservedHeaderMode { mode: 3 }));
2738 }
2739
2740 /// A shaper's tail-drop takes the same guards a hook's elide takes, and
2741 /// reports the same refusal — but no `ActionApplied`, because no hook
2742 /// asked for anything.
2743 ///
2744 /// The two halves are the two obligations on `DropTail`:
2745 /// an admitted drop moves the cursor and counts one elide, and a
2746 /// refused one leaves the cursor alone so the caller can admit the unit
2747 /// anyway. The `applied()` assertion is what separates this from
2748 /// `execute(.., Drop(Elide), ..)`: routing a configured drop through
2749 /// the hook path would emit a per-object `ActionApplied` naming a
2750 /// decision nobody made.
2751 ///
2752 /// *Ablation, recorded:* have `shape_elide` return `true`
2753 /// unconditionally (skip `admit_kind`) — the refusal half reddens on
2754 /// `assert!(!exec::shape_elide(..))`, and, in the integration fixture,
2755 /// a status object would be silently destroyed.
2756 #[test]
2757 fn a_shaper_tail_drop_takes_the_elide_guards_and_reports_only_refusals() {
2758 for &draft in COMPILED_DRAFTS {
2759 // Admitted: an ordinary object.
2760 let h = Harness::new();
2761 let m = meta(draft);
2762 assert!(
2763 shape_elide(&object_unit(&m, Instant::now()), &h.report()),
2764 "draft {draft:?}: an ordinary object elides"
2765 );
2766 assert_eq!(h.counters.snapshot().objects_elided, 1, "draft {draft:?}");
2767 assert_eq!(h.counters.snapshot().actions_refused, 0, "draft {draft:?}");
2768 assert!(h.observer.applied().is_empty(), "no hook asked, so nothing was applied");
2769
2770 // Refused: a status object, on every draft.
2771 let h = Harness::new();
2772 let mut m = meta(draft);
2773 m.status = Some(3);
2774 m.payload_len = 0;
2775 assert!(
2776 !shape_elide(&object_unit(&m, Instant::now()), &h.report()),
2777 "draft {draft:?}: a status object may not be elided, so the shaper \
2778 must admit the unit instead"
2779 );
2780 assert_eq!(h.counters.snapshot().objects_elided, 0, "draft {draft:?}");
2781 assert_eq!(
2782 h.observer.refused(),
2783 vec![(Site::Object, ActionKind::DropElide, Refusal::WouldDestroyStatusObject)],
2784 "draft {draft:?}: the refusal is reported, once",
2785 );
2786 assert_eq!(h.counters.snapshot().actions_refused, 1, "draft {draft:?}");
2787 }
2788 }
2789
2790 #[test]
2791 fn eliding_a_status_object_is_refused() {
2792 for &draft in COMPILED_DRAFTS {
2793 let mut h = Harness::new();
2794 let mut m = meta(draft);
2795 m.status = Some(3);
2796 m.payload_len = 0;
2797 let out = h.run(&object_unit(&m, Instant::now()), Action::Drop(DropMode::Elide));
2798 assert_eq!(out.result, Err(Refusal::WouldDestroyStatusObject), "draft {draft:?}");
2799 }
2800 }
2801
2802 #[test]
2803 fn an_admitted_elide_writes_nothing_counts_one_and_owes_a_cursor_move() {
2804 for &draft in COMPILED_DRAFTS {
2805 let mut h = Harness::new();
2806 let m = meta(draft);
2807 let at = Instant::now();
2808 let renumbered = elide_renumbers_successor(&object_unit(&m, at));
2809 let out = h.run(&object_unit(&m, at), Action::Drop(DropMode::Elide));
2810 assert_eq!(out.plan, Plan::Nothing, "draft {draft:?}");
2811 assert_eq!(
2812 out.result,
2813 Ok(Effect::Elided { renumbered_successor: renumbered }),
2814 "draft {draft:?}",
2815 );
2816 assert!(out.note_elided);
2817 assert_eq!(h.counters.snapshot().objects_elided, 1);
2818 }
2819 }
2820
2821 /// A deferral is counted where it is decided, and a forwarded unit is
2822 /// not counted at all.
2823 ///
2824 /// The figure `Action::Delay` did not used to have. The one that was
2825 /// meant to answer for a deferral sat on the shaping statistics, where
2826 /// every figure is gated on a configured profile — so on the sessions a
2827 /// hook alone impairs, which need no profile whatever, it could only
2828 /// ever have read zero.
2829 ///
2830 /// *Ablation, run:* delete the `ActionKind::Delay` arm from
2831 /// `Reporter::applied`.
2832 ///
2833 /// ```text
2834 /// assertion `left == right` failed: draft Draft07: the engine took the unit
2835 /// off the wire and queued it for a later release, and the figure for what
2836 /// it deferred did not move
2837 /// left: 0
2838 /// right: 1
2839 /// ```
2840 ///
2841 /// Recorded without a `file:line` prefix: editing this paragraph moves
2842 /// the line it would name.
2843 #[test]
2844 fn a_deferred_unit_is_counted_and_a_forwarded_one_is_not() {
2845 for &draft in COMPILED_DRAFTS {
2846 let m = meta(draft);
2847 let mut h = Harness::new();
2848 let out = h.run(
2849 &object_unit(&m, Instant::now()),
2850 Action::Delay { by: Duration::from_millis(5), then: Box::new(Action::Pass) },
2851 );
2852 assert!(matches!(out.result, Ok(Effect::Queued { .. })), "draft {draft:?}");
2853 let counted = h.counters.snapshot();
2854 assert_eq!(
2855 counted.units_delayed, 1,
2856 "draft {draft:?}: the engine took the unit off the wire and queued it \
2857 for a later release, and the figure for what it deferred did not move",
2858 );
2859 assert_eq!(counted.actions_refused, 0, "draft {draft:?}");
2860 assert_eq!(
2861 counted.objects_truncated, 0,
2862 "draft {draft:?}: a deferral is not a truncation, and one figure \
2863 answering for both would be indistinguishable from either",
2864 );
2865
2866 let mut h = Harness::new();
2867 h.run(&object_unit(&m, Instant::now()), Action::Pass);
2868 assert_eq!(
2869 h.counters.snapshot().units_delayed,
2870 0,
2871 "draft {draft:?}: a unit that went straight out was never deferred",
2872 );
2873 }
2874 }
2875
2876 /// The figure is `units_delayed` and not `objects_delayed`, and a
2877 /// deferred **control frame** is the whole reason.
2878 ///
2879 /// `classify_control` honours `Delay` on every compiled draft, so a
2880 /// SUBSCRIBE held back is a real deferral with no object anywhere in it.
2881 /// A counter named for objects would have had to either miss this or
2882 /// count it under a name that does not describe it. Its neighbour keeps
2883 /// the object name honestly: a truncation is refused on a control
2884 /// stream and can land nowhere but an object, which the gate below
2885 /// checks rather than assumes.
2886 #[test]
2887 fn a_deferred_control_frame_moves_the_same_figure() {
2888 for &draft in COMPILED_DRAFTS {
2889 let mut h = Harness::new();
2890 let out = h.run(
2891 &control_unit(draft, Instant::now()),
2892 Action::Delay { by: Duration::from_millis(5), then: Box::new(Action::Pass) },
2893 );
2894 assert!(matches!(out.result, Ok(Effect::Queued { .. })), "draft {draft:?}");
2895 assert_eq!(
2896 h.counters.snapshot().units_delayed,
2897 1,
2898 "draft {draft:?}: a control frame is a unit, and it was deferred",
2899 );
2900 }
2901 }
2902
2903 /// A truncation is counted when it is applied, and never when it is
2904 /// refused.
2905 ///
2906 /// Two refusals, because they are refused by different code and a
2907 /// counter placed at the attempt rather than the application would pass
2908 /// one of them and fail the other. The error code is checked inside the
2909 /// `Truncate` arm itself; the site is checked by `classify`, before the
2910 /// arm is reached at all.
2911 ///
2912 /// *Ablation, run:* delete the `ActionKind::Truncate` arm from
2913 /// `Reporter::applied`.
2914 ///
2915 /// ```text
2916 /// assertion `left == right` failed: draft Draft07: the object went out cut
2917 /// short to its first three bytes and nothing counted it
2918 /// left: 0
2919 /// right: 1
2920 /// ```
2921 #[test]
2922 fn a_truncation_counts_when_it_is_applied_and_never_when_it_is_refused() {
2923 for &draft in COMPILED_DRAFTS {
2924 let m = meta(draft);
2925 let mut h = Harness::new();
2926 let out =
2927 h.run(&object_unit(&m, Instant::now()), Action::Truncate { bytes: 3, code: 0x2 });
2928 assert!(matches!(out.result, Ok(Effect::Truncated { .. })), "draft {draft:?}");
2929 assert_eq!(
2930 h.counters.snapshot().objects_truncated,
2931 1,
2932 "draft {draft:?}: the object went out cut short to its first three \
2933 bytes and nothing counted it",
2934 );
2935
2936 let mut h = Harness::new();
2937 let out = h.run(
2938 &object_unit(&m, Instant::now()),
2939 Action::Truncate { bytes: 3, code: MAX_APPLICATION_ERROR_CODE + 1 },
2940 );
2941 assert!(out.result.is_err(), "draft {draft:?}");
2942 let counted = h.counters.snapshot();
2943 assert_eq!(
2944 counted.objects_truncated, 0,
2945 "draft {draft:?}: an out-of-range code refuses the truncation, and \
2946 a refused action cut nothing short",
2947 );
2948 assert_eq!(counted.actions_refused, 1, "draft {draft:?}");
2949
2950 let mut h = Harness::new();
2951 let out = h.run(
2952 &control_unit(draft, Instant::now()),
2953 Action::Truncate { bytes: 3, code: 0x2 },
2954 );
2955 assert_eq!(out.result, Err(Refusal::ControlStreamResetIllegal), "draft {draft:?}");
2956 assert_eq!(
2957 h.counters.snapshot().objects_truncated,
2958 0,
2959 "draft {draft:?}: the object name holds because the control site \
2960 refuses the action outright",
2961 );
2962 }
2963 }
2964
2965 /// A session nobody is watching still counts what it did.
2966 ///
2967 /// `Reporter` caches `observer.wants_events()` and gates **events** on
2968 /// it; the counters are outside that gate, on the same terms
2969 /// `Reporter::refused` states for its own. The claim is worth a gate
2970 /// rather than a comment because the failure is silent in the direction
2971 /// that matters: a counter bumped inside the gate agrees with its event
2972 /// in every test that has an observer attached, which is all of them,
2973 /// and reads zero only in production.
2974 ///
2975 /// *Ablation, run:* move both bumps inside `Reporter::emit`'s `enabled`
2976 /// check by writing them as `if self.enabled { .. }`.
2977 ///
2978 /// ```text
2979 /// assertion `left == right` failed: draft Draft07: a deferral on a session
2980 /// nobody attached to is still a deferral, and this figure is the only
2981 /// thing that says so
2982 /// left: 0
2983 /// right: 1
2984 /// ```
2985 #[test]
2986 fn a_session_with_nobody_watching_still_counts_what_it_applied() {
2987 for &draft in COMPILED_DRAFTS {
2988 let m = meta(draft);
2989 let mut h = Harness::new();
2990 h.run_unwatched(
2991 &object_unit(&m, Instant::now()),
2992 Action::Delay { by: Duration::from_millis(5), then: Box::new(Action::Pass) },
2993 );
2994 let mut h2 = Harness::new();
2995 h2.run_unwatched(
2996 &object_unit(&m, Instant::now()),
2997 Action::Truncate { bytes: 3, code: 0x2 },
2998 );
2999
3000 assert!(
3001 h.observer.events().is_empty() && h2.observer.events().is_empty(),
3002 "draft {draft:?}: nothing was emitted, which is what makes the \
3003 counters below a measurement rather than a restatement",
3004 );
3005 assert_eq!(
3006 h.counters.snapshot().units_delayed,
3007 1,
3008 "draft {draft:?}: a deferral on a session nobody attached to is still \
3009 a deferral, and this figure is the only thing that says so",
3010 );
3011 assert_eq!(
3012 h2.counters.snapshot().objects_truncated,
3013 1,
3014 "draft {draft:?}: and so is a truncation",
3015 );
3016 }
3017 }
3018
3019 /// Draft-14 by name and by `#[cfg]`: `renumbered_successor: true` is
3020 /// asserted as a literal here rather than computed, so the fixture has
3021 /// to be a draft that delta-encodes Object IDs, and 14 is the first of
3022 /// them.
3023 #[cfg(feature = "draft14")]
3024 #[test]
3025 fn a_deferred_elide_still_moves_the_cursor_at_the_decision() {
3026 let mut h = Harness::new();
3027 let m = meta(DraftVersion::Draft14);
3028 let out = h.run(
3029 &object_unit(&m, Instant::now()),
3030 Action::Drop(DropMode::Elide).delayed(Duration::from_millis(20)),
3031 );
3032 assert!(
3033 out.note_elided,
3034 "the framer's cursor is positional; note_elided cannot wait for the release",
3035 );
3036 assert_eq!(
3037 h.deferred.pop(),
3038 Some(Deferred {
3039 action: ActionKind::DropElide,
3040 effect: Effect::Elided { renumbered_successor: true },
3041 }),
3042 );
3043 }
3044
3045 /// The one predicate this module duplicates from `framer.rs`
3046 /// (`ObjectFramer::elide_owes_a_fixup`, private there).
3047 ///
3048 /// The two stream kinds are the two halves of the claim and the boundary
3049 /// moves between them — draft-14 for a subgroup stream, draft-15 for a
3050 /// fetch one. A single number restated for both would be a table that
3051 /// happened to agree with the code on twelve of the twenty-six rows.
3052 ///
3053 /// Asserted against an explicitly restated table rather than against the
3054 /// framer itself, and that is a limitation worth stating: `note_elided`
3055 /// `debug_assert`s that the object it is handed is the one the framer
3056 /// most recently emitted, so a standalone probe cannot ask the framer
3057 /// this question without driving fourteen drafts of real wire bytes
3058 /// through it. The compensating cover is
3059 /// `tests/actions_objects.rs`, which asserts the *bytes* of an elided
3060 /// run against an independent encoder — a wrong answer here shows up
3061 /// there as a wrong `renumbered_successor` on a stream whose bytes
3062 /// disagree.
3063 ///
3064 /// Swept over [`ALL_DRAFTS`] and not [`COMPILED_DRAFTS`] on purpose:
3065 /// `elide_renumbers_successor` reads the [`DraftVersion`] value and
3066 /// nothing else, so every row of the table is answerable in every
3067 /// build, and restating all fourteen is the whole point of the test.
3068 #[test]
3069 fn elide_renumbering_names_the_drafts_that_owe_a_fixup() {
3070 for draft in ALL_DRAFTS {
3071 for stream_kind in [DataStreamType::Subgroup, DataStreamType::Fetch] {
3072 let mut m = meta(draft);
3073 m.stream_kind = stream_kind;
3074 let unit = object_unit(&m, Instant::now());
3075 let expected = match stream_kind {
3076 DataStreamType::Subgroup => draft.number() >= 14,
3077 DataStreamType::Fetch => draft.number() >= 15,
3078 };
3079 assert_eq!(
3080 elide_renumbers_successor(&unit),
3081 expected,
3082 "draft {draft:?} / {stream_kind:?}",
3083 );
3084 }
3085 }
3086 // A non-object site never renumbers anything.
3087 assert!(!elide_renumbers_successor(&control_unit(DraftVersion::Draft19, Instant::now())));
3088 }
3089
3090 #[test]
3091 fn truncate_queues_a_prefix_then_a_reset_and_reports_the_prefix_length() {
3092 for &draft in COMPILED_DRAFTS {
3093 let mut h = Harness::new();
3094 let m = meta(draft);
3095 let out =
3096 h.run(&object_unit(&m, Instant::now()), Action::Truncate { bytes: 3, code: 0x2 });
3097 assert_eq!(out.plan, Plan::Terminal, "draft {draft:?}");
3098 // `code_defined` is the reset vocabulary's business, not this
3099 // test's; `drafts_07_to_10_report_the_reset_code_as_undefined`
3100 // restates that table against the draft numbers themselves.
3101 assert_eq!(
3102 out.result,
3103 Ok(Effect::Truncated {
3104 forwarded: 3,
3105 code: 0x2,
3106 code_defined: stream_reset_code_defined(draft),
3107 }),
3108 "draft {draft:?}",
3109 );
3110 assert_eq!(h.pending.len(), 1);
3111 assert!(h.pending.head_release().is_some());
3112 }
3113 }
3114
3115 #[test]
3116 fn truncate_past_the_end_of_the_unit_forwards_the_whole_unit() {
3117 for &draft in COMPILED_DRAFTS {
3118 let mut h = Harness::new();
3119 let m = meta(draft);
3120 let out =
3121 h.run(&object_unit(&m, Instant::now()), Action::Truncate { bytes: 9_999, code: 0 });
3122 assert_eq!(
3123 out.result,
3124 Ok(Effect::Truncated {
3125 forwarded: object_bytes().len(),
3126 code: 0,
3127 code_defined: stream_reset_code_defined(draft),
3128 }),
3129 "draft {draft:?}",
3130 );
3131 }
3132 }
3133
3134 #[test]
3135 fn drafts_07_to_10_report_the_reset_code_as_undefined() {
3136 for &draft in COMPILED_DRAFTS {
3137 // Restated here rather than read from `stream_reset_code_defined`,
3138 // which is the thing under test, and exhaustive for the reason
3139 // `capability.rs`'s `a_first_object_carrier_exists` gives: a
3140 // fifteenth draft must not join either side of the partition
3141 // without an answer being written down twice.
3142 let expected = match draft {
3143 DraftVersion::Draft07
3144 | DraftVersion::Draft08
3145 | DraftVersion::Draft09
3146 | DraftVersion::Draft10 => false,
3147 DraftVersion::Draft11
3148 | DraftVersion::Draft12
3149 | DraftVersion::Draft13
3150 | DraftVersion::Draft14
3151 | DraftVersion::Draft15
3152 | DraftVersion::Draft16
3153 | DraftVersion::Draft17
3154 | DraftVersion::Draft18
3155 | DraftVersion::Draft19
3156 | DraftVersion::Draft20 => true,
3157 };
3158 let mut h = Harness::new();
3159 let m = meta(draft);
3160 let out = h.run(&object_unit(&m, Instant::now()), Action::ResetStream { code: 5 });
3161 assert_eq!(
3162 out.result,
3163 Ok(Effect::StreamReset { code: 5, code_defined: expected }),
3164 "draft {draft:?}",
3165 );
3166 }
3167 }
3168
3169 // ── the control site ────────────────────────────────────────────
3170
3171 /// The draft the single-row control fixtures use: the first this build
3172 /// compiled.
3173 ///
3174 /// `None` only in a build with no draft at all, where there is no
3175 /// control decoder and the rows below are not claims about this module.
3176 /// A hardcoded draft-11 was what a `--features draft07` build read as
3177 /// an executor failure when it was really the control site reporting
3178 /// that it has no decoder for draft 11.
3179 fn a_compiled_draft() -> Option<DraftVersion> {
3180 COMPILED_DRAFTS.first().copied()
3181 }
3182
3183 #[test]
3184 fn the_control_site_replaces_the_whole_frame_and_drops_it_whole() {
3185 let Some(draft) = a_compiled_draft() else { return };
3186 let mut h = Harness::new();
3187 let unit = control_unit(draft, Instant::now());
3188 let out = h.run(&unit, Action::Replace(Bytes::from_static(b"other")));
3189 assert_eq!(out.plan, Plan::WriteNow(Bytes::from_static(b"other")));
3190 assert_eq!(out.result, Ok(Effect::Replaced { bytes: 5 }));
3191
3192 let mut h = Harness::new();
3193 let out = h.run(&control_unit(draft, Instant::now()), Action::Drop(DropMode::Elide));
3194 assert_eq!(out.result, Ok(Effect::Dropped), "no object slot to renumber");
3195 assert!(!out.note_elided);
3196 assert_eq!(h.counters.snapshot().objects_elided, 0);
3197 }
3198
3199 /// Sweeps [`COMPILED_DRAFTS`] rather than [`ALL_DRAFTS`]: a refusal is
3200 /// what the engine hands a hook, and on a draft with no decoder no hook
3201 /// is reached, so `classify` answers `Unreachable` there instead. The
3202 /// claim being made is about the executor's rule, not about the
3203 /// build's.
3204 #[test]
3205 fn resetting_a_control_stream_is_refused_on_every_draft() {
3206 for &draft in COMPILED_DRAFTS {
3207 for action in [Action::ResetStream { code: 1 }, Action::Truncate { bytes: 1, code: 1 }]
3208 {
3209 let mut h = Harness::new();
3210 let out = h.run(&control_unit(draft, Instant::now()), action.clone());
3211 assert_eq!(
3212 out.result,
3213 Err(Refusal::ControlStreamResetIllegal),
3214 "draft {draft:?} / {action:?}",
3215 );
3216 assert!(h.pending.is_empty());
3217 }
3218 }
3219 }
3220
3221 #[test]
3222 fn drafts_17_to_20_execute_a_control_action_like_every_other_draft() {
3223 // These four carry the control plane on a pair of unidirectional
3224 // streams and requests on bidirectional ones. Both shapes reach this
3225 // site, so the column is `Yes` here exactly as it is on 07-16 and a
3226 // draft-conditional refusal would be wrong.
3227 //
3228 // Filtered to the compiled set, like every other control row: a
3229 // build without one of the four has no decoder for it and no hook
3230 // is reached, which is a claim about the build rather than about
3231 // the control plane's shape.
3232 for draft in [
3233 DraftVersion::Draft17,
3234 DraftVersion::Draft18,
3235 DraftVersion::Draft19,
3236 DraftVersion::Draft20,
3237 ]
3238 .into_iter()
3239 .filter(|d| COMPILED_DRAFTS.contains(d))
3240 {
3241 let mut h = Harness::new();
3242 let out = h.run(
3243 &control_unit(draft, Instant::now()),
3244 Action::Replace(Bytes::from_static(b"z")),
3245 );
3246 assert_eq!(out.result, Ok(Effect::Replaced { bytes: 1 }), "draft {draft:?}");
3247 }
3248 }
3249
3250 // ── the datagram site ───────────────────────────────────────────
3251
3252 #[test]
3253 fn a_datagram_replace_is_admitted_and_its_failure_is_the_callers_to_report() {
3254 let h = Harness::new();
3255 let unit = datagram_unit(DraftVersion::Draft11, Some(3));
3256 let report = h.datagram_report();
3257 let mut engine = h.datagram_engine();
3258 let out =
3259 execute(&unit, Action::Replace(Bytes::from_static(b"bigger")), &mut engine, &report);
3260 assert_eq!(out.result, Ok(Effect::Replaced { bytes: 6 }));
3261 assert_eq!(out.plan, Plan::WriteNow(Bytes::from_static(b"bigger")));
3262
3263 // The transport then rejects it: exactly one ActionFailed, and no
3264 // ActionApplied is retracted.
3265 report.failed(Site::Datagram, ActionKind::Replace, "too large".to_owned());
3266 let failed: Vec<_> = h
3267 .observer
3268 .events()
3269 .into_iter()
3270 .filter(|e| matches!(e, ProxyEvent::ActionFailed { .. }))
3271 .collect();
3272 assert_eq!(failed.len(), 1);
3273 assert_eq!(h.counters.snapshot().actions_refused, 0);
3274 }
3275
3276 #[test]
3277 fn a_datagram_payload_splice_needs_a_real_boundary() {
3278 // Delimited: spliced after the header.
3279 let h = Harness::new();
3280 let unit = datagram_unit(DraftVersion::Draft11, Some(3));
3281 let report = h.datagram_report();
3282 let mut engine = h.datagram_engine();
3283 let out = execute(
3284 &unit,
3285 Action::ReplacePayload(Bytes::from_static(b"NEWP")),
3286 &mut engine,
3287 &report,
3288 );
3289 let Plan::WriteNow(bytes) = out.plan else { panic!("expected a datagram write") };
3290 assert_eq!(&bytes[..3], &[0x01, 0x02, 0x03]);
3291 assert_eq!(&bytes[3..], b"NEWP");
3292
3293 // The three cases where there is no boundary.
3294 let cases = [
3295 (DraftVersion::Draft14, Some(3), false, "draft-14 header decode consumes the payload"),
3296 (DraftVersion::Draft11, None, false, "datagram header did not decode"),
3297 (DraftVersion::Draft11, Some(3), true, "status datagram has no payload"),
3298 ];
3299 for (draft, header_len, is_status, detail) in cases {
3300 let h = Harness::new();
3301 let unit = Unit {
3302 target: Target::Datagram {
3303 raw: Bytes::from_static(&[0x01, 0x02, 0x03, b'p']),
3304 header_len,
3305 is_status,
3306 },
3307 draft,
3308 arrived_at: Instant::now(),
3309 };
3310 let report = h.datagram_report();
3311 let mut engine = h.datagram_engine();
3312 let out = execute(
3313 &unit,
3314 Action::ReplacePayload(Bytes::from_static(b"N")),
3315 &mut engine,
3316 &report,
3317 );
3318 assert_eq!(
3319 out.result,
3320 Err(Refusal::PayloadNotDelimited { detail }),
3321 "draft {draft:?} / header_len {header_len:?} / status {is_status}",
3322 );
3323 assert_eq!(out.plan, Plan::WriteNow(Bytes::from_static(&[0x01, 0x02, 0x03, b'p'])),);
3324 }
3325 }
3326
3327 #[test]
3328 fn timing_and_terminals_are_refused_at_the_datagram_site() {
3329 for action in [
3330 Action::Pass.delayed(Duration::from_millis(1)),
3331 Action::Pass.held(Gate::new()),
3332 Action::Truncate { bytes: 1, code: 1 },
3333 Action::ResetStream { code: 1 },
3334 ] {
3335 let h = Harness::new();
3336 let unit = datagram_unit(DraftVersion::Draft11, Some(3));
3337 let report = h.datagram_report();
3338 let mut engine = h.datagram_engine();
3339 let out = execute(&unit, action.clone(), &mut engine, &report);
3340 assert!(
3341 matches!(out.result, Err(Refusal::WrongSite { site: Site::Datagram, .. })),
3342 "{action:?} -> {:?}",
3343 out.result,
3344 );
3345 }
3346 }
3347
3348 // ── the stream-end site ─────────────────────────────────────────
3349
3350 #[test]
3351 fn close_session_is_honoured_at_both_stream_end_columns() {
3352 for is_control_stream in [false, true] {
3353 let mut h = Harness::new();
3354 let out = h.run(
3355 &stream_end_unit(DraftVersion::Draft11, is_control_stream),
3356 Action::CloseSession { code: 3, reason: Bytes::from_static(b"x") },
3357 );
3358 assert_eq!(
3359 out.result,
3360 Ok(Effect::SessionClosing { code: 3 }),
3361 "control={is_control_stream}",
3362 );
3363 }
3364 }
3365
3366 #[test]
3367 fn reset_at_stream_end_is_a_data_stream_capability_only() {
3368 let mut h = Harness::new();
3369 let out =
3370 h.run(&stream_end_unit(DraftVersion::Draft11, false), Action::ResetStream { code: 4 });
3371 assert_eq!(out.plan, Plan::Terminal);
3372 assert_eq!(out.result, Ok(Effect::StreamReset { code: 4, code_defined: true }),);
3373
3374 let mut h = Harness::new();
3375 let out =
3376 h.run(&stream_end_unit(DraftVersion::Draft11, true), Action::ResetStream { code: 4 });
3377 assert_eq!(out.result, Err(Refusal::ControlStreamResetIllegal));
3378 assert_eq!(out.plan, Plan::Nothing, "a stream end carries no unit to forward");
3379 }
3380
3381 #[test]
3382 fn everything_else_at_stream_end_is_wrong_site_except_the_two_reset_shapes() {
3383 // Every unsupported action at the stream-end site is refused as
3384 // `WrongSite`, on both the control and the data stream — except
3385 // Truncate and ResetStream on a control stream, which have a
3386 // refusal of their own.
3387 for is_control_stream in [false, true] {
3388 for action in [
3389 Action::Replace(Bytes::from_static(b"x")),
3390 Action::ReplacePayload(Bytes::from_static(b"x")),
3391 Action::Pass.delayed(Duration::from_millis(1)),
3392 Action::Pass.held(Gate::new()),
3393 Action::Drop(DropMode::Elide),
3394 ] {
3395 let mut h = Harness::new();
3396 let out = h.run(
3397 &stream_end_unit(DraftVersion::Draft11, is_control_stream),
3398 action.clone(),
3399 );
3400 assert!(
3401 matches!(out.result, Err(Refusal::WrongSite { site: Site::StreamEnd, .. })),
3402 "control={is_control_stream} {action:?} -> {:?}",
3403 out.result,
3404 );
3405 }
3406 let mut h = Harness::new();
3407 let out = h.run(
3408 &stream_end_unit(DraftVersion::Draft11, is_control_stream),
3409 Action::Truncate { bytes: 1, code: 1 },
3410 );
3411 let expected = if is_control_stream {
3412 Refusal::ControlStreamResetIllegal
3413 } else {
3414 Refusal::WrongSite { site: Site::StreamEnd, action: ActionKind::Truncate }
3415 };
3416 assert_eq!(out.result, Err(expected));
3417 }
3418 }
3419
3420 #[test]
3421 fn pass_at_stream_end_changes_nothing() {
3422 let mut h = Harness::new();
3423 let out = h.run(&stream_end_unit(DraftVersion::Draft11, true), Action::Pass);
3424 assert_eq!(out.plan, Plan::Nothing);
3425 assert_eq!(out.result, Ok(Effect::ForwardedVerbatim));
3426 assert_eq!(h.observer.applied().len(), 1);
3427 }
3428
3429 // ── the stream-decision sites ───────────────────────────────────
3430
3431 #[test]
3432 fn stream_open_and_reject_each_report_once() {
3433 for site in [StreamSite::Open, StreamSite::Header] {
3434 let h = Harness::new();
3435 let out = execute_stream(site, DraftVersion::Draft11, StreamAction::Open, &h.report());
3436 assert_eq!(out.plan, Plan::Nothing);
3437 assert_eq!(out.result, Ok(Effect::ForwardedVerbatim));
3438 assert_eq!(h.observer.applied().len(), 1);
3439 assert_eq!(h.observer.applied()[0].0, site.site());
3440
3441 let h = Harness::new();
3442 let out = execute_stream(
3443 site,
3444 DraftVersion::Draft11,
3445 StreamAction::Reject { code: 9 },
3446 &h.report(),
3447 );
3448 assert_eq!(out.plan, Plan::RejectStream { code: 9 });
3449 assert_eq!(out.result, Ok(Effect::StreamRejected { code: 9 }));
3450 assert_eq!(h.observer.applied().len(), 1);
3451 }
3452 }
3453
3454 // ── invariants the rest of the file leans on ────────────────────
3455
3456 #[test]
3457 fn no_fact_precondition_survives_execution() {
3458 // `admit_conditional`'s `debug_assert` is only a claim if this
3459 // passes: sweep every site with fully-populated targets and assert
3460 // `classify` never asks for a fact `Target` did not supply.
3461 let mut saw_conditional = 0usize;
3462 for draft in ALL_DRAFTS {
3463 for stream_kind in [DataStreamType::Subgroup, DataStreamType::Fetch] {
3464 for index_in_stream in [0u64, 3] {
3465 for subgroup_id in [None, Some(0u64)] {
3466 for status in [None, Some(3u64)] {
3467 for mode in [None, Some(0u8), Some(1), Some(3)] {
3468 let mut m = meta(draft);
3469 m.stream_kind = stream_kind;
3470 m.index_in_stream = index_in_stream;
3471 m.subgroup_id = subgroup_id;
3472 m.status = status;
3473 let unit = Unit {
3474 target: Target::Object {
3475 meta: &m,
3476 subgroup_id_mode: mode,
3477 raw: object_bytes(),
3478 },
3479 draft,
3480 arrived_at: Instant::now(),
3481 };
3482 for kind in [
3483 ActionKind::Pass,
3484 ActionKind::ReplacePayload,
3485 ActionKind::DropElide,
3486 ActionKind::Truncate,
3487 ActionKind::ResetStream,
3488 ActionKind::CloseSession,
3489 ] {
3490 let replacement_len =
3491 (kind == ActionKind::ReplacePayload).then_some(4);
3492 let cx = unit.target.cap_ctx(draft, replacement_len);
3493 if let Support::Conditional(p) =
3494 classify(Site::Object, kind, &cx)
3495 {
3496 saw_conditional += 1;
3497 assert!(
3498 matches!(p, Precondition::WithinMaxDatagramSize),
3499 "unsupplied fact {p:?} at Object/{kind:?} \
3500 draft {draft:?}",
3501 );
3502 }
3503 }
3504 }
3505 }
3506 }
3507 }
3508 }
3509
3510 for is_status in [false, true] {
3511 for header_len in [None, Some(3usize)] {
3512 let unit = Unit {
3513 target: Target::Datagram {
3514 raw: Bytes::from_static(&[1, 2, 3, 4]),
3515 header_len,
3516 is_status,
3517 },
3518 draft,
3519 arrived_at: Instant::now(),
3520 };
3521 for kind in [
3522 ActionKind::Pass,
3523 ActionKind::Replace,
3524 ActionKind::ReplacePayload,
3525 ActionKind::DropElide,
3526 ActionKind::CloseSession,
3527 ] {
3528 let cx = unit.target.cap_ctx(draft, Some(1));
3529 if let Support::Conditional(p) = classify(Site::Datagram, kind, &cx) {
3530 saw_conditional += 1;
3531 assert!(
3532 matches!(p, Precondition::WithinMaxDatagramSize),
3533 "unsupplied fact {p:?} at Datagram/{kind:?} draft {draft:?}",
3534 );
3535 }
3536 }
3537 }
3538 }
3539 }
3540 assert!(
3541 saw_conditional > 0,
3542 "the sweep must reach the environmental preconditions, or it proves nothing",
3543 );
3544 }
3545
3546 #[test]
3547 fn the_ledger_stays_the_same_length_as_the_queue() {
3548 for &draft in COMPILED_DRAFTS {
3549 let mut h = Harness::new();
3550 let m = meta(draft);
3551 let at = Instant::now();
3552 let actions = [
3553 Action::Pass.delayed(Duration::from_millis(30)),
3554 Action::Pass,
3555 Action::Drop(DropMode::Elide),
3556 Action::ReplacePayload(Bytes::from_static(b"abcd")),
3557 Action::Replace(Bytes::from_static(b"refused")),
3558 Action::ResetStream { code: 1 },
3559 ];
3560 for action in actions {
3561 h.run(&object_unit(&m, at), action);
3562 assert_eq!(
3563 h.deferred.len(),
3564 h.pending.len(),
3565 "[{draft:?}] one ledger entry per pushed unit, always",
3566 );
3567 }
3568 assert!(h.pending.len() >= 5, "draft {draft:?}");
3569 }
3570 }
3571
3572 #[test]
3573 fn backpressure_is_reported_once_per_stream() {
3574 for &draft in COMPILED_DRAFTS {
3575 let mut h = Harness::with_config(EgressConfig {
3576 max_pending_bytes: 16,
3577 max_hold: Duration::from_secs(30),
3578 ..EgressConfig::default()
3579 });
3580 let m = meta(draft);
3581 let at = Instant::now();
3582 h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_secs(5)));
3583 let mut transitions = 0;
3584 for _ in 0..6 {
3585 let out = h.run(&object_unit(&m, at), Action::Pass);
3586 if out.entered_backpressure {
3587 transitions += 1;
3588 }
3589 }
3590 assert_eq!(transitions, 1, "[{draft:?}] the transition is reported once");
3591 assert_eq!(
3592 h.observer
3593 .impairments()
3594 .iter()
3595 .filter(|k| matches!(k, ImpairmentKind::EgressQueueFull { .. }))
3596 .count(),
3597 1,
3598 );
3599 }
3600 }
3601
3602 #[test]
3603 fn a_delay_beyond_max_hold_is_clamped_and_reported_once() {
3604 for &draft in COMPILED_DRAFTS {
3605 let mut h = Harness::with_config(EgressConfig {
3606 max_pending_bytes: 1 << 20,
3607 max_hold: Duration::from_millis(50),
3608 ..EgressConfig::default()
3609 });
3610 let m = meta(draft);
3611 let at = Instant::now();
3612 let out = h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_secs(9)));
3613 let clamped = out.clamped.expect("a clamp happened");
3614 assert!(clamped.was_clamped(), "draft {draft:?}");
3615 assert_eq!(clamped.requested, Duration::from_secs(9));
3616 assert_eq!(clamped.applied, Duration::from_millis(50));
3617 assert_eq!(
3618 h.observer.impairments(),
3619 vec![ImpairmentKind::HoldClamped {
3620 requested: Some(Duration::from_secs(9)),
3621 applied: Duration::from_millis(50),
3622 }],
3623 );
3624
3625 // An unclamped delay reports nothing.
3626 let mut h = Harness::new();
3627 let out = h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_millis(5)));
3628 assert_eq!(out.clamped.map(|d| d.was_clamped()), Some(false), "draft {draft:?}");
3629 assert!(h.observer.impairments().is_empty());
3630 }
3631 }
3632
3633 #[test]
3634 fn the_ledger_forgets_what_a_failed_drain_could_not_write() {
3635 for &draft in COMPILED_DRAFTS {
3636 let mut h = Harness::new();
3637 let m = meta(draft);
3638 let at = Instant::now();
3639 h.run(
3640 &object_unit(&m, at),
3641 Action::ReplacePayload(Bytes::from_static(b"abcd"))
3642 .delayed(Duration::from_millis(10)),
3643 );
3644 assert_eq!(h.deferred.len(), 1, "draft {draft:?}");
3645 h.deferred.clear();
3646 assert!(h.deferred.is_empty(), "no Replaced is reported for bytes never written");
3647 assert!(h.deferred.take_all().is_empty());
3648 }
3649 }
3650
3651 #[test]
3652 fn take_all_yields_only_the_entries_that_owe_an_event() {
3653 for &draft in COMPILED_DRAFTS {
3654 let mut h = Harness::new();
3655 let m = meta(draft);
3656 let at = Instant::now();
3657 h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_millis(30)));
3658 h.run(&object_unit(&m, at), Action::Pass);
3659 h.run(
3660 &object_unit(&m, at),
3661 Action::ReplacePayload(Bytes::from_static(b"wxyz"))
3662 .delayed(Duration::from_millis(40)),
3663 );
3664 assert_eq!(h.deferred.len(), 3, "draft {draft:?}");
3665 let owed = h.deferred.take_all();
3666 assert_eq!(owed.len(), 2, "the ordering-only unit owes nothing");
3667 assert_eq!(owed[0].action, ActionKind::Pass);
3668 assert_eq!(owed[1].effect, Effect::Replaced { bytes: 10 });
3669 assert!(h.deferred.is_empty());
3670 }
3671 }
3672
3673 #[test]
3674 fn kind_of_agrees_with_the_published_attempt_mapping() {
3675 assert_eq!(kind_of(&Action::Pass), ActionKind::Pass);
3676 assert_eq!(kind_of(&Action::Replace(Bytes::new())), ActionKind::Replace);
3677 assert_eq!(kind_of(&Action::ReplacePayload(Bytes::new())), ActionKind::ReplacePayload,);
3678 assert_eq!(kind_of(&Action::Pass.delayed(Duration::ZERO)), ActionKind::Delay);
3679 assert_eq!(kind_of(&Action::Pass.held(Gate::new())), ActionKind::Hold);
3680 assert_eq!(kind_of(&Action::Drop(DropMode::Elide)), ActionKind::DropElide);
3681 assert_eq!(kind_of(&Action::Truncate { bytes: 0, code: 0 }), ActionKind::Truncate,);
3682 assert_eq!(kind_of(&Action::ResetStream { code: 0 }), ActionKind::ResetStream);
3683 assert_eq!(
3684 kind_of(&Action::CloseSession { code: 0, reason: Bytes::new() }),
3685 ActionKind::CloseSession,
3686 );
3687 }
3688
3689 // ── The impairment surface: ordering, and the leg ───────────────
3690 //
3691 // The three tests below are the gate for a single rule — an impairment
3692 // is emitted *after* what it reports, never before — and for the field
3693 // that says which of the proxy's two connections it is about. Both are
3694 // driven through `execute` and `Reporter`, the same two calls every
3695 // forwarding pipe makes, rather than by handing a `ProxyEvent` to an
3696 // observer directly: what is being checked is where the emission sits
3697 // relative to the work, which a hand-built event cannot show.
3698
3699 /// An action that is refused emits **no impairment at all**, even when
3700 /// the refused action's own arithmetic would have produced one.
3701 ///
3702 /// This is the rule in its sharpest form. `Delay { by: 9s }` against a
3703 /// 50 ms `max_hold` is a clamp by any reading of the numbers, and the
3704 /// clamp arithmetic is cheap enough that computing it early would look
3705 /// harmless. But the inner `ReplacePayload` is refused — seven bytes
3706 /// where the object's payload is four — so nothing is queued, nothing is
3707 /// held, and no wait is shortened. An observer told otherwise would have
3708 /// recorded a hold that was never applied to a unit that was forwarded
3709 /// unchanged, and there is no later event that takes an impairment back.
3710 ///
3711 /// The `Pass` case beside it is the control: the identical delay, with
3712 /// an inner action that is admitted, does clamp and does report. Without
3713 /// it the assertion would also pass against an engine that had stopped
3714 /// reporting clamps altogether.
3715 ///
3716 /// *Ablation, recorded:* in `plan_action`'s `Action::Delay` arm, hoist
3717 /// the `egress::defer_by` call and the `if deferral.was_clamped()`
3718 /// report above `let content = prepare_content(...)?;`, so the clamp is
3719 /// computed and reported before the inner action is judged. This test
3720 /// goes red with the real message
3721 ///
3722 /// ```text
3723 /// assertion `left == right` failed: [Draft07] a refused action changed
3724 /// nothing, so it impaired nothing
3725 /// left: [HoldClamped { requested: Some(9s), applied: 50ms }]
3726 /// right: []
3727 /// ```
3728 ///
3729 /// which is exactly the failure the rule exists to prevent: a hold
3730 /// reported against a unit that was forwarded verbatim.
3731 #[test]
3732 fn a_refused_action_reports_its_refusal_and_no_impairment() {
3733 for &draft in COMPILED_DRAFTS {
3734 let clamping = EgressConfig {
3735 max_pending_bytes: 1 << 20,
3736 max_hold: Duration::from_millis(50),
3737 ..EgressConfig::default()
3738 };
3739 let m = meta(draft);
3740 let at = Instant::now();
3741
3742 let mut h = Harness::with_config(clamping);
3743 let out = h.run(
3744 &object_unit(&m, at),
3745 // Seven bytes into a four-byte payload: `ReplacePayload` is
3746 // length-preserving at the object site, so the inner action
3747 // is refused before anything is queued.
3748 Action::ReplacePayload(Bytes::from_static(b"toolong"))
3749 .delayed(Duration::from_secs(9)),
3750 );
3751 assert!(!out.is_applied(), "[{draft:?}] the inner action is refused");
3752 assert_eq!(
3753 h.observer.impairments(),
3754 vec![],
3755 "[{draft:?}] a refused action changed nothing, so it impaired nothing",
3756 );
3757 assert_eq!(
3758 h.observer.refused().len(),
3759 1,
3760 "[{draft:?}] and the refusal itself is still reported",
3761 );
3762
3763 // The control: the same delay, admitted, does clamp and does say
3764 // so — so the emptiness above is the refusal and not silence.
3765 let mut h = Harness::with_config(clamping);
3766 let out = h.run(&object_unit(&m, at), Action::Pass.delayed(Duration::from_secs(9)));
3767 assert!(out.is_applied(), "[{draft:?}] a plain delay is admitted");
3768 assert_eq!(
3769 h.observer.impairments(),
3770 vec![ImpairmentKind::HoldClamped {
3771 requested: Some(Duration::from_secs(9)),
3772 applied: Duration::from_millis(50),
3773 }],
3774 );
3775 }
3776 }
3777
3778 /// Two impairments from one admitted action arrive in the order the
3779 /// engine did the work, both attributed to the connection being written
3780 /// to.
3781 ///
3782 /// A `Delay` beyond `max_hold` onto a queue smaller than the unit does
3783 /// two separate things — it shortens the wait, then it fills the queue —
3784 /// and each owes its own report. Asserted as a `Vec` rather than as two
3785 /// counts because the order is a claim: the clamp is decided while the
3786 /// unit is still being planned, and backpressure is only knowable once
3787 /// the push has happened. Reversed, the pair would say the queue was
3788 /// already full before the unit that filled it went in.
3789 ///
3790 /// Both name [`Leg::Upstream`] on a pipe reading from the client,
3791 /// because both are about the queue in front of the *relay* connection.
3792 /// A reader who took the event's `side` for a connection would attribute
3793 /// them to the client leg — the one this proxy was reading from and the
3794 /// one that is behaving perfectly.
3795 ///
3796 /// *Ablation, recorded:* in `event::impairment_leg`, move
3797 /// `EgressQueueFull` and `HoldClamped` out of the departure arm into the
3798 /// arrival arm — `Some(here)`. This test goes red with the real message
3799 ///
3800 /// ```text
3801 /// assertion `left == right` failed: [Draft07] the clamp is decided
3802 /// before the push and both belong to the connection being written to
3803 /// left: [(Some(Client), HoldClamped { requested: Some(9s), applied:
3804 /// 50ms }), (Some(Client), EgressQueueFull { stream_id: 4 })]
3805 /// right: [(Some(Upstream), HoldClamped { requested: Some(9s),
3806 /// applied: 50ms }), (Some(Upstream), EgressQueueFull {
3807 /// stream_id: 4 })]
3808 /// ```
3809 #[test]
3810 fn one_action_owes_two_impairments_in_the_order_it_earned_them() {
3811 for &draft in COMPILED_DRAFTS {
3812 let mut h = Harness::with_config(EgressConfig {
3813 // Below the ten-byte unit, so the very first push crosses
3814 // the limit and the transition is reported on it.
3815 max_pending_bytes: 4,
3816 max_hold: Duration::from_millis(50),
3817 ..EgressConfig::default()
3818 });
3819 let m = meta(draft);
3820 let out = h.run(
3821 &object_unit(&m, Instant::now()),
3822 Action::Pass.delayed(Duration::from_secs(9)),
3823 );
3824 assert!(out.is_applied(), "[{draft:?}] the delay is admitted");
3825 assert_eq!(
3826 h.observer.attributed_impairments(),
3827 vec![
3828 (
3829 Some(Leg::Upstream),
3830 ImpairmentKind::HoldClamped {
3831 requested: Some(Duration::from_secs(9)),
3832 applied: Duration::from_millis(50),
3833 },
3834 ),
3835 (Some(Leg::Upstream), ImpairmentKind::EgressQueueFull { stream_id: 4 }),
3836 ],
3837 "[{draft:?}] the clamp is decided before the push and both belong to the \
3838 connection being written to",
3839 );
3840 }
3841 }
3842
3843 /// The same impairment names the other connection when it is raised by
3844 /// the other pipe, and a report that is about neither connection names
3845 /// neither.
3846 ///
3847 /// The first half is what makes the field worth carrying: `HoldClamped`
3848 /// is `Leg::Upstream` on the pipe reading from the client and
3849 /// `Leg::Client` on the pipe reading from the relay, because in both
3850 /// cases it is the far side that is being written to. The second half is
3851 /// [`ImpairmentKind::CoarseReleaseTimer`], which is a fact about the
3852 /// host's clock: it is equally true of both legs, stays true if one goes
3853 /// away, and so answers `None` rather than being pinned to whichever
3854 /// pipe noticed the coarse tick first.
3855 ///
3856 /// *Ablation, recorded:* in `event::impairment_leg`, move
3857 /// `CoarseReleaseTimer` from the `None` arm into the departure arm. This
3858 /// test goes red with the real message
3859 ///
3860 /// ```text
3861 /// assertion `left == right` failed: the release wheel belongs to the
3862 /// process, not to a connection, so the pipe reading from
3863 /// ClientToProxy must not name one either
3864 /// left: Some(Upstream)
3865 /// right: None
3866 /// ```
3867 ///
3868 /// — a host-clock fact attributed to the relay connection, and it would
3869 /// have been attributed to the client connection had the other pipe
3870 /// reported it first.
3871 #[test]
3872 fn the_leg_turns_with_the_pipe_and_is_absent_where_there_is_none() {
3873 let draft = COMPILED_DRAFTS[0];
3874 let m = meta(draft);
3875 let clamping = EgressConfig {
3876 max_pending_bytes: 1 << 20,
3877 max_hold: Duration::from_millis(50),
3878 ..EgressConfig::default()
3879 };
3880
3881 for (side, expected) in
3882 [(ProxySide::ClientToProxy, Leg::Upstream), (ProxySide::RelayToProxy, Leg::Client)]
3883 {
3884 let mut h = Harness::with_config(clamping).reading_from(side);
3885 h.run(&object_unit(&m, Instant::now()), Action::Pass.delayed(Duration::from_secs(9)));
3886 assert_eq!(
3887 h.observer.attributed_impairments(),
3888 vec![(
3889 Some(expected),
3890 ImpairmentKind::HoldClamped {
3891 requested: Some(Duration::from_secs(9)),
3892 applied: Duration::from_millis(50),
3893 },
3894 )],
3895 "a clamp on the pipe reading from {side:?} holds bytes off the {expected:?} leg",
3896 );
3897 }
3898
3899 // Both pipes, because *equally true of both legs* is the claim. A
3900 // single side would pass against a mapping that answers whichever
3901 // connection the reporting pipe happens to be on — which is the guess
3902 // this arm exists to refuse.
3903 for side in [ProxySide::ClientToProxy, ProxySide::RelayToProxy] {
3904 let h = Harness::new().reading_from(side);
3905 h.report().impairment(ImpairmentKind::CoarseReleaseTimer {
3906 backend: crate::instrument::TimerBackend::Condvar,
3907 detail: None,
3908 });
3909 let attributed = h.observer.attributed_impairments();
3910 assert_eq!(attributed.len(), 1);
3911 assert_eq!(
3912 attributed[0].0, None,
3913 "the release wheel belongs to the process, not to a connection, so the pipe \
3914 reading from {side:?} must not name one either",
3915 );
3916 }
3917 }
3918
3919 /// One pipe, three reports, three different answers — as an exact
3920 /// sequence rather than a set.
3921 ///
3922 /// This is the shape the whole surface is for. A single forwarding task
3923 /// reading from the client raises a parser failure about the bytes that
3924 /// arrived, a queue failure about the bytes it is trying to place, and a
3925 /// host fact about neither, and the three have to come back attributed
3926 /// to `Client`, `Upstream` and nothing respectively. Compared as a `Vec`
3927 /// because the order is part of the claim: an impairment is emitted
3928 /// after what it reports, so the sequence is the order the proxy did
3929 /// things in, and a set would pass against a proxy that reported them
3930 /// backwards.
3931 ///
3932 /// *Ablation, recorded:* in `event::impairment_leg`, fold
3933 /// `FramerBypass` and `ObjectNotAddressable` into the departure arm, so
3934 /// every report answers the far connection. This test goes red with the
3935 /// real message
3936 ///
3937 /// ```text
3938 /// assertion `left == right` failed: one pipe, three answers: what
3939 /// arrived, what could not be written, and neither
3940 /// left: [(Some(Upstream), FramerBypass { stream_id: 4, draft:
3941 /// Draft07, reason: DecodeError }), (Some(Upstream),
3942 /// EgressQueueFull { stream_id: 4 }), (None, CoarseReleaseTimer
3943 /// { backend: Condvar, detail: None })]
3944 /// right: [(Some(Client), FramerBypass { stream_id: 4, draft: Draft07,
3945 /// reason: DecodeError }), (Some(Upstream), EgressQueueFull {
3946 /// stream_id: 4 }), (None, CoarseReleaseTimer { backend:
3947 /// Condvar, detail: None })]
3948 /// ```
3949 ///
3950 /// — a stream the proxy could not parse on the way *in*, blamed on the
3951 /// connection it was writing out to.
3952 #[test]
3953 fn one_pipe_reports_the_near_leg_the_far_leg_and_neither_in_order() {
3954 let draft = COMPILED_DRAFTS[0];
3955 let h = Harness::new();
3956 let report = h.report();
3957
3958 report.impairment(ImpairmentKind::FramerBypass {
3959 stream_id: 4,
3960 draft,
3961 reason: crate::types::BypassReason::DecodeError,
3962 });
3963 report.impairment(ImpairmentKind::EgressQueueFull { stream_id: 4 });
3964 report.impairment(ImpairmentKind::CoarseReleaseTimer {
3965 backend: crate::instrument::TimerBackend::Condvar,
3966 detail: None,
3967 });
3968
3969 assert_eq!(
3970 h.observer.attributed_impairments(),
3971 vec![
3972 (
3973 Some(Leg::Client),
3974 ImpairmentKind::FramerBypass {
3975 stream_id: 4,
3976 draft,
3977 reason: crate::types::BypassReason::DecodeError,
3978 },
3979 ),
3980 (Some(Leg::Upstream), ImpairmentKind::EgressQueueFull { stream_id: 4 }),
3981 (
3982 None,
3983 ImpairmentKind::CoarseReleaseTimer {
3984 backend: crate::instrument::TimerBackend::Condvar,
3985 detail: None,
3986 },
3987 ),
3988 ],
3989 "one pipe, three answers: what arrived, what could not be written, and neither",
3990 );
3991 }
3992}