moqtap_proxy/control.rs
1//! The control plane — a live handle onto a running proxy.
2//!
3//! Everything else in this crate is configured before it starts: a
4//! [`ListenerConfig`](crate::listener::ListenerConfig) is consumed when the
5//! endpoint is bound, a [`ProxySessionConfig`](crate::session::ProxySessionConfig)
6//! is copied once per accepted connection, and a
7//! [`ProxyHook`](crate::hook::ProxyHook) declares its interest once at
8//! session start. That is deliberate — it is what makes a run reproducible
9//! from the values that started it — but it leaves no way to ask a proxy
10//! that is *already running* anything at all.
11//!
12//! [`ProxyControl`] is that way. It is obtained from
13//! [`TransparentProxy::control`](crate::proxy::TransparentProxy::control)
14//! **before** the accept loop is awaited, because the caller that owns the
15//! proxy is usually the caller that is about to give up its thread of
16//! control to `run()`; a handle that could only be taken afterwards could
17//! not be taken at all. Everything it reports is therefore defined for a
18//! proxy that has not bound yet: [`ProxyControl::local_addr`] answers
19//! [`ProxyError::NotBound`] until the listener exists, and
20//! [`ProxyControl::sessions`] answers an empty list.
21//!
22//! # Two pieces of shared state, both released by a guard
23//!
24//! The handle reads two things the proxy used to keep as stack locals of
25//! `run()`: the bound listener, and the set of sessions that are live right
26//! now. Both are published into this module when they come into existence
27//! and removed when they go, and in both cases the removal is a `Drop`
28//! rather than a call at each exit site.
29//!
30//! That choice is the whole correctness argument for
31//! [`ProxyControl::sessions`]. A session ends for at least five distinct
32//! reasons — a forwarding task returned, a forwarding task errored, the peer
33//! went away, a hook asked for a close, the proxy was cancelled — and a
34//! sixth that no enumeration covers, the session's whole future being
35//! dropped by whoever spawned it. A list of removal sites is only ever as
36//! complete as the reader who wrote it, and the failure it produces is
37//! silent: `sessions()` keeps naming an id nothing can act on, and every
38//! later call that takes that id fails in a way that looks like a race.
39//! `Drop` is complete by construction, so the registration is handed out as
40//! a `SessionGuard` and lives in the frame of the function that runs the
41//! session.
42//!
43//! A third piece is held here and released by nothing: the proxy-wide
44//! shaping counters behind [`ProxyControl::stats`]. They are the one report
45//! in this module that is *cumulative* rather than instantaneous, and that
46//! is exactly why they cannot be assembled from the two above. A total
47//! summed over the session list would fall every time a client disconnected,
48//! because the list is released by a `Drop` and a session that has ended
49//! leaves nothing behind — and a statistic that goes down under normal
50//! operation cannot be alerted on. So each session is handed the counters
51//! when it attaches, charges them as it forwards, and leaves them behind
52//! when it goes.
53//!
54//! # Reaching a session that is already running
55//!
56//! Registering an id is enough to *list* a session but not to *act* on one.
57//! Acting needs the session's cancellation token, its close request slot,
58//! its stream registry, its two control-stream inboxes and its egress
59//! knobs — and every one of those is built by the session before it dials
60//! the upstream relay, which is what lets the whole entry go into the
61//! registry at the top of the run function. A session spends its longest
62//! single operation connecting, sometimes for as long as its connect
63//! timeout allows and sometimes forever; one that only became reachable
64//! afterwards would be unreachable for exactly that long.
65//!
66//! What is deliberately **not** in an entry is either connection. The two
67//! `Transport`s are owned by the forwarding scope, and a table scanned by
68//! every list call has no business holding them alive past the session that
69//! owns them.
70//!
71//! The channels are created with the session rather than attached later
72//! because a task attached after the fact could never cover the sessions
73//! that were already running when it was attached — which is precisely the
74//! set a control plane exists to reach.
75//!
76//! # Three levels of reach, and why they are not one mechanism
77//!
78//! [`ProxyControl::close_session`] acts on the whole session, so it goes to
79//! the session's own command task: it is the only request that has to
80//! *wait* for something — the bounded egress drain — and a waiting request
81//! needs somewhere to wait that is not the caller's thread.
82//!
83//! [`ProxyControl::reset_stream`] and [`ProxyControl::inject_control`] act on
84//! one stream, and neither waits. They resolve in the caller: the registry
85//! entry carries the session's `StreamRegistry`, so a key that names nothing
86//! live is refused synchronously, and a key that names a live stream is reached
87//! by dropping a `StreamCommand` into that stream's own inbox. Routing them
88//! through the session task as well would have added a hop and a second place
89//! for a request to be lost, and would have made *there is no such stream* an
90//! answer that arrives asynchronously — which a synchronous method signature
91//! cannot deliver.
92//!
93//! Nothing here accepts a request and discards it. Every method either
94//! delivers to a task that will act on it or answers a [`ControlError`]
95//! saying why it could not.
96//!
97//! # Reconfiguring, and the one verb that cannot reach a live connection
98//!
99//! Three of the requests here change what the proxy *is* rather than what a
100//! particular session is doing, and they reach three different distances.
101//! The distances are not a matter of how they were written; they are what
102//! the thing being changed will admit.
103//!
104//! `ProxyControl::set_impair` reaches traffic already in flight. It acts
105//! below QUIC, on the datagrams a leg's socket is about to pass, and the
106//! next one out carries the new profile.
107//!
108//! [`ProxyControl::set_shape`] and [`ProxyControl::set_shaper_enabled`]
109//! reach a running session. The switch is read on the next release decision
110//! a queue makes, which for a stream held by a bucket that will refill is
111//! within one pacing interval and for a stream held by a bucket configured
112//! at zero is not until the `max_hold` clamp — see the method for why
113//! nothing here can do better. A new profile is taken up by a running
114//! session at its next stream, because a stream's egress queue holds the
115//! scheduler its units were admitted under and a class is an index into that
116//! scheduler's class list.
117//!
118//! [`ProxyControl::set_transport`] reaches **no connection that already
119//! exists, ever**. A QUIC connection takes its transport configuration once,
120//! at setup, and keeps it for life — quinn offers four setters on a live
121//! connection and no way to replace the configuration behind it. So a
122//! profile set on the relay leg reaches the next connection this proxy
123//! *opens*, which is the next session it accepts, and a profile set on the
124//! client leg reaches the next connection it *accepts*, which the proxy does
125//! not initiate and which may never arrive. On a proxy that never dials or
126//! accepts again — every client already connected, nothing new coming — the
127//! call is a permanent silent no-op that returns `Ok(())`, and no return
128//! value here distinguishes that from a setting that reached everything
129//! afterwards. It is written out on the method, in this module, and in the
130//! crate documentation, in those words, because a caller who reads it as
131//! "set the window" and measures the session in front of them will measure
132//! the old window and believe the new one.
133//!
134//! # Two kinds of name here are deliberately not links
135//!
136//! `SessionGuard`, `StreamRegistry` and `StreamCommand` are crate-internal,
137//! and `set_impair` and `clear_impair` exist only under the `impair` feature.
138//! This page is public and is built without that feature, so a link to
139//! either kind resolves to nothing — and an unresolved intra-doc link is an
140//! error under the documentation build, not a warning. Both are written in
141//! plain code font instead, so the prose can still name the thing it is
142//! describing without breaking that build. Turning one back into a link is
143//! what reddens it.
144
145use std::collections::HashMap;
146use std::net::SocketAddr;
147use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
148use std::sync::{Arc, Mutex};
149use std::time::Duration;
150
151use bytes::Bytes;
152use tokio::sync::mpsc;
153use tokio::task::JoinHandle;
154use tokio_util::sync::CancellationToken;
155
156use crate::action::{EgressConfig, Gate};
157use crate::egress::SessionCloser;
158use crate::error::ProxyError;
159use crate::event::SessionId;
160use crate::listener::Listener;
161use crate::shape::{ProxyRecorder, ProxyStats, ShapeError, ShapeProfile, StreamKey};
162use crate::transport::{
163 DefaultInstaller, TransportInstaller, TransportProfile, TransportProfileError,
164};
165use crate::types::Leg;
166
167/// How many commands a session's inbox holds before a sender has to wait.
168///
169/// Small on purpose. A control caller issues one request and waits for its
170/// answer, so depth beyond a handful only ever buys the ability to queue
171/// work behind a session that has stopped serving its inbox — which is a
172/// session that is on its way down, and a queued command aimed at it should
173/// be refused rather than parked.
174pub(crate) const COMMAND_QUEUE_DEPTH: usize = 16;
175
176/// Why a control-plane request could not be carried out.
177///
178/// Distinct from [`ProxyError`], which describes a proxy that could not be
179/// built or a connection that could not be made. Every variant here is a
180/// request the proxy understood and declined, and each says which of the
181/// three reasons applied: the thing addressed is not there, the thing asked
182/// for cannot be done on that leg over that transport, or the value supplied
183/// was refused by the same check that would have refused it in a
184/// configuration file.
185///
186/// Neither `Eq` nor `#[non_exhaustive]`, and both omissions are load-bearing.
187/// `Eq` is unavailable because [`TransportProfileError`] reaches an `f32`.
188/// `#[non_exhaustive]` is absent so that a caller outside this crate can
189/// match every variant with no wildcard arm — a match that stops compiling
190/// when a variant is added, which is the one place a new refusal reliably
191/// gets noticed.
192///
193/// One variant is `#[cfg]`-gated: `ControlError::Impairment` exists only
194/// under the `impair` feature, which is also the only build in which the
195/// method that returns it exists. A downstream exhaustive match therefore
196/// carries the same `#[cfg(feature = "impair")]` on that arm. Reaching for a
197/// `_` arm to avoid the attribute would satisfy the feature-on build as well
198/// and swallow every later variant with it, which is the whole of what the
199/// missing `#[non_exhaustive]` buys. It is written in plain code font rather
200/// than linked, because a link from this always-compiled page would not
201/// resolve in a build without the feature — the same convention the crate
202/// page uses for every other feature-gated name.
203#[derive(Debug, Clone, PartialEq, thiserror::Error)]
204pub enum ControlError {
205 /// No session with this id has ever run on this proxy.
206 ///
207 /// Session ids are minted per [`TransparentProxy`](crate::proxy::TransparentProxy)
208 /// and are not comparable across proxies, so an id from one proxy handed
209 /// to another lands here rather than acting on an unrelated session.
210 /// A session that ran and has since ended answers
211 /// [`ControlError::SessionEnded`] instead; the two are told apart by the
212 /// highest id this proxy has ever registered, which is enough because
213 /// ids are minted monotonically.
214 ///
215 /// The one id that is here and *did* exist: a connection accepted but
216 /// not yet running. `SessionStarted` is emitted at accept and
217 /// registration happens when the session begins to run, so an id in that
218 /// window is in no registry — see [`ProxyControl::sessions`], which says
219 /// the same thing about the census. Which of the two refusals such an id
220 /// gets is not fixed: the split is made on the highest id ever
221 /// registered, and two connections accepted together may reach their run
222 /// functions in either order, so an id awaiting registration answers
223 /// this unless a *later* one registered first, in which case it answers
224 /// [`ControlError::SessionEnded`]. Neither answer is retryable and
225 /// neither acts on anything, so the difference is one of wording rather
226 /// than of what a caller may then do.
227 #[error("no session {0:?}")]
228 NoSuchSession(SessionId),
229 /// This proxy ran a session with this id and is no longer running it,
230 /// or is running it but has stopped taking requests for it.
231 ///
232 /// The ordinary race, and not a fault: a session may end between the
233 /// call that listed it and the call that acts on it, and a control
234 /// plane that panicked there could not be driven from a timeline. Every
235 /// verb that names a [`SessionId`] can answer this.
236 ///
237 /// Separate from [`ControlError::NoSuchSession`] because the two mean
238 /// different things to a caller holding a list: a lookup miss on an id
239 /// this proxy never issued says the list came from somewhere else,
240 /// while this says the list was right and the session ended underneath
241 /// the call. Neither is retryable.
242 ///
243 /// It also carries one case that is neither: a request that could not
244 /// be *taken*, because the target already has
245 /// [`COMMAND_QUEUE_DEPTH`](self) requests outstanding and has served
246 /// none of them. That is a session or a stream that is not going to
247 /// serve them, and the frozen refusal set has no "try again later"
248 /// answer to give instead. Reaching it takes sixteen unanswered
249 /// requests aimed at one target.
250 #[error("session {0:?} has already ended")]
251 SessionEnded(SessionId),
252 /// The session is live but has no stream under this key.
253 ///
254 /// A key that names a stream which has already ended is reported the
255 /// same way as one that never existed. The two are not distinguished
256 /// anywhere in this crate: a stream's registration is removed as it
257 /// ends, so there is no record left to tell them apart, and inventing
258 /// one would mean holding every finished stream's identity for the life
259 /// of the session.
260 #[error("no stream {stream:?} on session {id:?}")]
261 NoSuchStream {
262 /// The session the stream was looked for on.
263 id: SessionId,
264 /// The key that matched nothing live.
265 stream: StreamKey,
266 },
267 /// The request is meaningful in general but cannot be carried out on
268 /// this leg, because of what that leg's transport is able to expose.
269 ///
270 /// Reported rather than ignored. A leg that quietly declined would leave
271 /// the caller holding a successful return and a proxy that behaves as
272 /// though nothing was asked — the failure this crate is least willing to
273 /// ship, because it is invisible from the outside and survives every
274 /// green run.
275 #[error("{what} is unsupported on the {leg:?} leg over {transport}")]
276 Unsupported {
277 /// What was asked for, in the words a caller would use for it.
278 what: &'static str,
279 /// The leg it was asked of.
280 leg: Leg,
281 /// That leg's transport, named as a caller would recognise it —
282 /// `"WebTransport"`, `"QUIC"`.
283 transport: &'static str,
284 },
285 /// A [`TransportProfile`] was
286 /// refused.
287 ///
288 /// The same check that runs when a profile is supplied in a
289 /// configuration, reporting the same error, so a profile that a leg
290 /// would not have started with is not one it can be moved to.
291 #[error(transparent)]
292 Profile(#[from] TransportProfileError),
293 /// A [`ShapeProfile`] was refused.
294 ///
295 /// As with [`ControlError::Profile`], this is the construction-time
296 /// check rather than a second, looser one.
297 #[error(transparent)]
298 Shape(#[from] ShapeError),
299 /// A datagram impairment profile was refused by `quinn-netem`, and this
300 /// is the reason it gave.
301 ///
302 /// Present only under the `impair` feature, which is also the only build
303 /// in which [`ProxyControl::set_impair`] exists — the variant and the
304 /// method that produces it are gated together, so there is no build
305 /// carrying a refusal nothing can return.
306 ///
307 /// # Why it is not [`ControlError::Unsupported`]
308 ///
309 /// It was, and the loss was the whole reason. `quinn_netem::ProfileError`
310 /// distinguishes thirteen ways a profile is armed and does nothing — a
311 /// reorder model on a direction with no delay, a token bucket whose burst
312 /// is below one datagram, a peer filter that matches no peer — and every
313 /// one of them arrived here as the same sentence naming the leg. The
314 /// caller was then told to re-run
315 /// `quinn_netem::ImpairProfile::validate` to find out what had actually
316 /// happened, which is a workaround for a type that could not express the
317 /// answer, written into the documentation as though it were a workflow.
318 /// `Unsupported` also says something that is not true here. That variant
319 /// means the request cannot be carried out **on this leg over this
320 /// transport**; a refused impairment profile is refused on every leg and
321 /// every transport, because it is the profile that is wrong. A caller
322 /// matching on the two now gets one arm for *this proxy cannot impair that
323 /// leg*, which is fixed by supplying a socket, and one for *this profile is
324 /// not armable*, which is fixed by editing the profile.
325 ///
326 /// A refused profile changes nothing: whatever was armed on that leg
327 /// before the call is still armed.
328 #[cfg(feature = "impair")]
329 #[error("the impairment profile for the {leg:?} leg was refused: {source}")]
330 Impairment {
331 /// The leg the profile was aimed at.
332 leg: Leg,
333 /// Why `quinn-netem` would not arm it.
334 source: quinn_netem::ProfileError,
335 },
336}
337
338/// A handle onto a running proxy.
339///
340/// Cheap to clone — one `Arc` bump — and every clone reads and writes the
341/// same state, so a handle may be moved into a task, kept beside the proxy,
342/// or both. It holds no lock across an `await` and takes no part in the
343/// forwarding path.
344///
345/// Obtained from
346/// [`TransparentProxy::control`](crate::proxy::TransparentProxy::control),
347/// which can be called at any point after the proxy is constructed —
348/// including before
349/// [`TransparentProxy::run`](crate::proxy::TransparentProxy::run) is
350/// awaited, which is the usual case, because `run()` does not return until
351/// the proxy is finished.
352///
353/// # What it reports
354///
355/// * [`ProxyControl::local_addr`] — the address the listener bound, or
356/// [`ProxyError::NotBound`] before there is one.
357/// * [`ProxyControl::sessions`] — the ids of the sessions that are live at this
358/// instant.
359/// * [`ProxyControl::stats`] — what this proxy's shaping has done across every
360/// session it has accepted, ended ones included, cleared by
361/// [`ProxyControl::reset_stats`]. The one report here that is cumulative
362/// rather than instantaneous, which is why it survives the disconnect that
363/// removes a session from the list above.
364///
365/// The first two are snapshots of a proxy that keeps moving. A session named by
366/// `sessions()` may end before the next line of the caller's code runs;
367/// that is not a defect in the snapshot but the reason every request that
368/// takes a [`SessionId`] can answer [`ControlError::SessionEnded`].
369///
370/// # What it changes, and how far each change reaches
371///
372/// * `ProxyControl::set_impair` / `ProxyControl::clear_impair` — the next
373/// datagram that leg's socket passes, in either direction. The only verb here
374/// that reaches traffic already flowing over a connection that already
375/// exists.
376/// * [`ProxyControl::set_shaper_enabled`] — the next release decision each
377/// queue makes, on every session already running.
378/// * [`ProxyControl::set_shape`] — the next stream a running session forwards,
379/// and every session accepted afterwards.
380/// * [`ProxyControl::set_transport`] — the next connection the leg makes or
381/// accepts, and **never** one that already exists.
382/// * [`ProxyControl::close_session`], [`ProxyControl::reset_stream`],
383/// [`ProxyControl::inject_control`] — one named session, or one stream of it,
384/// immediately.
385#[derive(Debug, Clone)]
386pub struct ProxyControl {
387 plane: Arc<ControlPlane>,
388}
389
390impl ProxyControl {
391 /// Wrap a proxy's control state in a handle.
392 pub(crate) fn new(plane: Arc<ControlPlane>) -> Self {
393 Self { plane }
394 }
395
396 /// The address this proxy's listener is bound to.
397 ///
398 /// [`ProxyError::NotBound`] until
399 /// [`TransparentProxy::run`](crate::proxy::TransparentProxy::run) has
400 /// bound the endpoint, and again once it has returned and the endpoint
401 /// is gone. Between those two points this is the address a client
402 /// connects to, which is the reason the method exists: a proxy
403 /// configured with port 0 chooses its port inside `run()`, and before
404 /// this there was no way to learn which port that was without binding
405 /// the socket in the caller and handing it in.
406 ///
407 /// A bound listener can still fail to report its address — the query
408 /// goes to the operating system — and that failure is reported as
409 /// [`ProxyError::Listener`], distinct from not being bound at all.
410 pub fn local_addr(&self) -> Result<SocketAddr, ProxyError> {
411 match self.plane.bound() {
412 Some(listener) => listener.local_addr(),
413 None => Err(ProxyError::NotBound),
414 }
415 }
416
417 /// The ids of the sessions that are live right now.
418 ///
419 /// An id appears here when its session starts running and disappears
420 /// when that session ends, for any reason at all — including the ones
421 /// no teardown site could be written for. It is **not** a log: a
422 /// session that has ended leaves nothing behind, so a caller that wants
423 /// a history of everything the proxy handled reads
424 /// [`ProxyEvent::SessionStarted`](crate::event::ProxyEvent::SessionStarted)
425 /// and
426 /// [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
427 /// from its observer instead.
428 ///
429 /// Sorted ascending, which makes the value comparable between two calls
430 /// without the caller sorting it first. Ids are minted monotonically per
431 /// proxy, so the order is also arrival order.
432 ///
433 /// # This list and the event stream do not have to agree
434 ///
435 /// `SessionStarted` is emitted when a connection is accepted, before the
436 /// session has dialled the upstream relay; registration here happens
437 /// when the session begins to run. A session whose upstream connect
438 /// fails is therefore reported as started, is briefly listed here, and
439 /// then disappears — and a session that has been accepted but has not
440 /// been polled yet is in the event stream and not in this list. Sessions
441 /// driven by constructing a
442 /// [`ProxySession`](crate::session::ProxySession) directly, rather than
443 /// through a proxy's accept loop, appear in neither: they belong to no
444 /// proxy and so to no control plane.
445 pub fn sessions(&self) -> Vec<SessionId> {
446 self.plane.live()
447 }
448
449 /// What this proxy's shaping has done, across every session it has
450 /// accepted — including the ones that have already ended.
451 ///
452 /// The complement of [`Self::sessions`], and the difference between them
453 /// is the whole reason this exists. That list is what is live *now* and
454 /// shrinks when a client disconnects; these figures are cumulative and
455 /// monotone, so a caller polling them across a disconnect sees them hold
456 /// rather than fall. A total summed over the live list could not have
457 /// that property: session registrations are released by a `Drop`, and a
458 /// statistic that goes down under normal operation cannot be alerted on.
459 ///
460 /// It follows that a session which ended for any of the reasons no
461 /// teardown site could be written for — its whole future dropped, its
462 /// task cancelled mid-write — has still contributed everything it moved,
463 /// because each unit was charged at the instant it was handled.
464 ///
465 /// Defined before the proxy has bound: a proxy that has accepted nothing
466 /// answers `ProxyStats::default()` rather than an error, the same shape
467 /// [`Self::sessions`] takes.
468 ///
469 /// # An all-zero answer means "no profile", not "no traffic"
470 ///
471 /// Every counter behind this is written by the shaping path, which a
472 /// session with no [`ShapeProfile`] never enters. A proxy forwarding
473 /// gigabytes with no profile configured reports
474 /// `ProxyStats::default()`, and nothing in the value distinguishes that
475 /// from a proxy nothing has connected to. Ask [`Self::sessions`], or an
476 /// observer's event stream, which of the two it is.
477 ///
478 /// # Sessions driven directly are not counted
479 ///
480 /// A [`ProxySession`](crate::session::ProxySession) constructed by a
481 /// caller instead of accepted by this proxy belongs to no control plane,
482 /// so it reports only its own
483 /// [`ShapeStats`](crate::shape::ShapeStats) — the same rule
484 /// [`Self::sessions`] follows, and for the same reason: a proxy must not
485 /// claim traffic it never accepted.
486 ///
487 /// # Cost
488 ///
489 /// A read of about forty relaxed atomics plus nine per class row, and
490 /// one `Vec` and one `String` allocated per class row. Cheap, and still
491 /// a reader's call rather than something to poll in a tight loop.
492 pub fn stats(&self) -> ProxyStats {
493 self.plane.stats.snapshot()
494 }
495
496 /// Zero every figure [`Self::stats`] reports, and start again from here.
497 ///
498 /// For a caller measuring a phase rather than a run: reset, drive the
499 /// traffic, read. Without it the only way to get a phase figure is to
500 /// subtract two snapshots, which is correct but leaves every assertion
501 /// written against a difference rather than a value.
502 ///
503 /// # What it does not do
504 ///
505 /// It does not touch any session's own
506 /// [`ShapeStats`](crate::shape::ShapeStats). Those are a session's for
507 /// its whole life, and a proxy-level verb that silently rewrote them
508 /// would make a scenario reading both see two different pasts.
509 ///
510 /// It does not clear the class rows themselves, only their counters. The
511 /// rows are sized once, from the first shaped session this proxy
512 /// accepts, because a class index means what the scheduler that produced
513 /// it says it means; dropping them here would let the next session
514 /// install a different class list and report figures under it.
515 ///
516 /// # It does not stop traffic
517 ///
518 /// The counters are cleared one relaxed store at a time while the
519 /// forwarding tasks keep charging them, so a reset that races a live
520 /// stream can land between two increments of the same unit and leave a
521 /// row part-cleared. There is no atomic form of this — no primitive
522 /// clears forty counters at once — and pausing the proxy to get one
523 /// would be a far larger promise than the figures are worth. Reset when
524 /// the traffic you are about to measure has not started.
525 pub fn reset_stats(&self) {
526 self.plane.stats.reset();
527 }
528
529 /// Set the QUIC transport parameters one leg uses — **for connections it
530 /// has not made yet**.
531 ///
532 /// # This never reaches a connection that already exists
533 ///
534 /// Read that sentence as the whole of what this method does, because
535 /// every other sentence here is a consequence of it. A QUIC connection
536 /// takes its transport configuration once, at setup, and holds it for
537 /// life; quinn exposes four setters on a connection that is already up —
538 /// the two stream-count limits and the two windows — and no way at all
539 /// to hand it a different configuration. So a profile set here reaches:
540 /// * on [`Leg::Upstream`], the next connection **this proxy opens**, which
541 /// is the next session it accepts, because a session dials the relay
542 /// once and then forwards over that connection until it ends;
543 ///
544 /// * on [`Leg::Client`], the next connection **this proxy accepts**, which
545 /// the proxy does not initiate and which may never arrive.
546 ///
547 /// It follows that on a proxy which never dials or accepts again — one
548 /// whose clients have all connected, one nothing is connecting to, one
549 /// about to be cancelled — this call is a **permanent silent no-op that
550 /// returns `Ok(())`**. Nothing about the return value distinguishes that
551 /// from a setting that reached every connection made afterwards, because
552 /// there is nothing to distinguish: the parameters were installed, and
553 /// what did or did not happen next is traffic rather than configuration.
554 /// A caller that needs the change to bite on a session already running
555 /// has one instrument, and it is
556 /// [`ProxyControl::close_session`] — a leg that reconnects is a leg
557 /// that installs this.
558 ///
559 /// # Every call is absolute: `None` is a default, not "leave alone"
560 ///
561 /// The profile is built into a **fresh** `quinn::TransportConfig` — a
562 /// `TransportConfig::default()` with the profile applied over it — and
563 /// that config is installed whole. So a field left `None` is not carried
564 /// over from the profile installed before it: it takes quinn's default.
565 ///
566 /// Two consecutive calls therefore do not compose. A call setting
567 /// `stream_receive_window` followed by a call setting only
568 /// `max_idle_timeout` leaves the leg with the idle timeout **and quinn's
569 /// default window**, not with both settings. `TransportProfile::default()`
570 /// puts a leg back on quinn's defaults entirely, which is the only way to
571 /// clear a setting and is why this is worth stating: a merge would look
572 /// like a convenience and would make `None` mean two different things —
573 /// "unset" when a profile is built and "unchanged" when it is installed —
574 /// leaving no profile that could ever clear a field, and no way for a
575 /// caller restoring normal conditions to say so.
576 ///
577 /// # What it replaces
578 ///
579 /// Whatever that leg's transport parameters were, whether they came from
580 /// a [`TransportProfile`] or from a raw `quinn::TransportConfig` in the
581 /// configuration the proxy was built with. The two cannot be combined —
582 /// see
583 /// [`ProxyError::TransportConfigAndProfile`]
584 /// — so a live profile that was merged with a configured raw config
585 /// would be a merge that cannot be written; a live profile that sat
586 /// beside one would turn every subsequent connect into that refusal.
587 /// Replacing is the only answer that leaves the leg usable, and it means
588 /// a caller who set a raw config at build time loses it from the first
589 /// call to this.
590 ///
591 /// The profile is built through the leg's own
592 /// [`TransportInstaller`] when the
593 /// proxy was given one, so a leg keeps building its configuration the
594 /// way it always did.
595 ///
596 /// # Errors
597 ///
598 /// [`ControlError::Profile`] for a profile the leg would not have
599 /// started with — the same check, reporting the same error, run here
600 /// rather than at the next connect, so a refusal is attached to the call
601 /// that caused it instead of to a connection failure minutes later.
602 ///
603 /// [`ControlError::Unsupported`] on [`Leg::Upstream`] when the upstream
604 /// is WebTransport. That endpoint is built inside the WebTransport
605 /// library, which takes no `quinn::TransportConfig` and hands back no
606 /// endpoint to install one on, so the profile could only be stored and
607 /// ignored.
608 pub fn set_transport(&self, leg: Leg, p: TransportProfile) -> Result<(), ControlError> {
609 match leg {
610 Leg::Client => {
611 let config = self.plane.build_transport(leg, &p)?;
612 self.plane.set_client_transport(config);
613 Ok(())
614 }
615 Leg::Upstream if self.plane.legs.upstream_webtransport => Err(
616 // Named exactly as the field on `ProxySessionConfig` names
617 // it, so the refusal and the setting it refuses read as the
618 // same thing.
619 ControlError::Unsupported {
620 what: "transport profile",
621 leg,
622 transport: "webtransport",
623 },
624 ),
625 Leg::Upstream => {
626 // Built and thrown away: the point is the refusal, which has
627 // to happen now rather than at the next dial. The profile
628 // itself is what is stored, so each session still runs the
629 // installer once for its own connection exactly as a
630 // configured profile does.
631 self.plane.build_transport(leg, &p)?;
632 *self.plane.upstream_transport.lock().expect("control plane") = Some(p);
633 Ok(())
634 }
635 }
636 }
637
638 /// Replace the shaping profile this proxy's sessions pace with.
639 ///
640 /// # What it reaches, and when
641 ///
642 /// Every session this proxy accepts afterwards shapes with `p`, whatever
643 /// the profile it was configured with.
644 ///
645 /// A session that is **already running** picks `p` up at the next stream
646 /// it forwards, not at the next object. That granularity is forced: a
647 /// stream's egress queue holds the scheduler its units were admitted
648 /// under, and a class is an index into that scheduler's class list, so a
649 /// unit classified under one profile and released under another charges
650 /// one profile's class against the other profile's buckets. Streams
651 /// already forwarding therefore run to their end under the profile they
652 /// started with, and on MoQT that is rarely a long wait — media arrives
653 /// on a fresh unidirectional stream per subgroup.
654 ///
655 /// A session that started **unshaped** stays unshaped for its whole
656 /// life, and nothing here changes that. Shaping classifies
657 /// [`ObjectMeta`](crate::framer::ObjectMeta), which only the object
658 /// framer produces, and whether a session frames at all is settled at
659 /// session start from the profile it was configured with. A session that
660 /// began as a byte pump has no objects to classify and no queue to pace.
661 ///
662 /// # A session whose class list would change keeps its own profile
663 ///
664 /// A running session takes `p` only if `p`'s class names are the same,
665 /// in the same order, as the ones the session started with. Otherwise it
666 /// keeps the profile it has until it ends, and only sessions accepted
667 /// afterwards get the new one.
668 ///
669 /// The reason is the statistics.
670 /// [`ShapeStats`](crate::shape::ShapeStats) has one row per configured
671 /// class, pre-sized when the session is constructed and never resized,
672 /// and a class is charged to its row **by position**. A profile with a
673 /// different class list would therefore charge its classes to rows still
674 /// named after the old profile's — every number in
675 /// [`ProxySession::shape_stats`](crate::session::ProxySession::shape_stats)
676 /// correct, and every label on it wrong — or, for a class beyond the end
677 /// of the original list, silently to the default row. Refusing the swap
678 /// per session is the only answer that keeps a reader's numbers
679 /// attributable. To move a running session onto a different class list,
680 /// end it with [`ProxyControl::close_session`]; its replacement starts
681 /// on the new profile.
682 ///
683 /// # Buckets start full
684 ///
685 /// A session that takes `p` builds a scheduler for it, and a fresh
686 /// scheduler's buckets are full as of that instant — the same choice a
687 /// session's first scheduler makes, so that a session does not open with
688 /// a burst-sized delay in front of its first object. Setting the same
689 /// profile repeatedly therefore hands every class a fresh burst each
690 /// time, and a caller doing that in a tight loop measures no rate limit
691 /// at all. The report-once diagnostics
692 /// (`ImpairmentKind::ShapeRuleUnmatchable`,
693 /// `ImpairmentKind::ShapeBurstBelowUnit`) start again with the new
694 /// scheduler too, so a rule that is unmatchable under both profiles is
695 /// reported once per profile rather than once per session.
696 ///
697 /// # Errors
698 ///
699 /// [`ControlError::Shape`] for a profile [`ShapeProfile::try_new`]
700 /// rejects. Since that constructor is the only way to build a
701 /// `ShapeProfile`, a profile arriving here has already passed it and the
702 /// refusal is unreachable today; the check is re-run rather than assumed
703 /// so that the rules live in exactly one place and a later construction
704 /// path cannot arrive here unchecked.
705 pub fn set_shape(&self, p: ShapeProfile) -> Result<(), ControlError> {
706 // Through the constructor, not around it: re-validating field by
707 // field here would be a second copy of the rules, and the two would
708 // drift in the direction that accepts something a session cannot
709 // run.
710 let checked = ShapeProfile::try_new(
711 p.buckets().to_vec(),
712 p.classes().to_vec(),
713 p.queue().clone(),
714 p.discipline(),
715 )?;
716 self.plane.shape.set(checked);
717 Ok(())
718 }
719
720 /// Turn pacing off, or back on, for every session this proxy is running
721 /// and every session it accepts afterwards.
722 ///
723 /// Off is not "no profile". The profile stays exactly where it was, the
724 /// classes keep claiming units, the per-stream queue depth keeps
725 /// applying and the statistics keep moving — what stops is the token
726 /// bucket and the discipline, so every unit is released as soon as the
727 /// queue reaches it. `set_shaper_enabled(true)` resumes with the same
728 /// configuration and the same counters, which is what makes this usable
729 /// as a switch in a timeline rather than as a way of throwing a profile
730 /// away.
731 ///
732 /// # When a stream that is already held resumes
733 ///
734 /// The switch is read on the next release decision a queue makes, and a
735 /// queue makes one when its head's wait expires. Switching pacing off
736 /// therefore does not reach into a wait that is already running; it
737 /// changes the answer the stream gets when that wait ends. How long that
738 /// is depends on why the stream was held:
739 /// * held by a bucket that will refill — one unit's worth of the
740 /// configured rate, so a stream paced at a real rate resumes within one
741 /// pacing interval;
742 ///
743 /// * held behind another class — as soon as that class drains, which is now
744 /// immediate;
745 /// * held by a bucket configured at **zero**, or one whose burst cannot
746 /// cover a unit — there is no refill instant, so the stream's wait is the
747 /// queue's `max_hold` clamp and the switch is not read until it fires. On
748 /// a stopped class, switching pacing off does **not** promptly release
749 /// what is already queued. Nothing in this crate can: the wait is a timer
750 /// a per-stream queue armed, and there is no session-wide wake that
751 /// reaches one. Use [`ProxyControl::set_shape`] to move the session onto
752 /// a profile with a rate, or end the session, if that is what is wanted.
753 ///
754 /// # It changes nothing on a proxy with no profile
755 ///
756 /// Returns nothing, and cannot fail, so it is silent about a proxy where
757 /// no session has a [`ShapeProfile`] to pace with — there is nothing to
758 /// switch and no consequence to report. That is the one case where
759 /// calling this has no observable effect whatever.
760 pub fn set_shaper_enabled(&self, on: bool) {
761 self.plane.shape.set_enabled(on);
762 }
763
764 /// Install a datagram impairment on one leg's socket.
765 ///
766 /// Effective from the next datagram that leg passes through its socket,
767 /// in both directions. It is the only verb here that reaches traffic
768 /// already in flight, because it acts below QUIC on the datagrams
769 /// themselves rather than on anything a connection settled at setup.
770 /// Datagrams already handed to the operating system are gone — the shim
771 /// sees a datagram once, on its way through, and a profile armed after that
772 /// cannot recall it. So *effective immediately* means the next datagram,
773 /// not the last one.
774 ///
775 /// The other leg is untouched. The two are separate sockets carrying
776 /// separate connections, and a profile armed on one says nothing about
777 /// the other.
778 ///
779 /// Arming replaces whatever was armed before, resets both directions'
780 /// sequence numbers and moves the tick origin to this instant, so a
781 /// recorded decision log reads from the moment of the call. It does not
782 /// clear the counters.
783 ///
784 /// # Errors
785 ///
786 /// [`ControlError::Unsupported`] when this proxy holds no impairment
787 /// handle for `leg` — which is every leg unless the socket and its
788 /// handle were handed over together before `run()`, and the refusal says
789 /// so and names the call that fixes it. This is the case that must not
790 /// be silent: a profile stored against a leg whose datagrams never cross
791 /// an impaired socket is armed, reported as applied, and applied to
792 /// nothing, and the run that follows looks clean because it *is* clean.
793 ///
794 /// [`ControlError::Impairment`] for a profile `quinn-netem` refuses,
795 /// carrying that crate's own reason unchanged — which of the thirteen
796 /// ways a profile arms and does nothing this one is. A caller therefore
797 /// learns what is wrong from the answer to the call that was wrong,
798 /// rather than by running `quinn_netem::ImpairProfile::validate` again
799 /// to find out. A refused profile changes nothing — whatever was armed
800 /// before is still armed.
801 ///
802 /// # No observer event, and that is a decision rather than an omission
803 /// Nothing is emitted here — not on arming, not on the first datagram the
804 /// profile touches. Three separate reasons, and the first alone settles it:
805 /// * **there is no session to name.** Every
806 /// [`ProxyEvent`](crate::event::ProxyEvent) variant carries a
807 /// [`SessionId`], and observers dispatch on it. A leg's socket carries
808 /// every session on that leg, including the ones this proxy has not
809 /// accepted yet — and those are precisely the sessions the profile will
810 /// impair for their whole lives. Fanning one event out over the sessions
811 /// that happen to be live would be a report that is silent about the
812 /// population it most affects.
813 /// * **the caller already knows.** This method is synchronous and its
814 /// return value is the answer. An event saying *the thing you just asked
815 /// for was accepted* tells an observer nothing the call site did not
816 /// have.
817 /// * **the consequence is not observable from here.** The shim acts below
818 /// QUIC, on datagrams that have already been encrypted and coalesced, so
819 /// nothing in this crate can say which session or which stream lost what.
820 /// `quinn-netem`'s own counters and decision log are the report for that,
821 /// and they are read through the handle rather than through an event
822 /// stream.
823 ///
824 /// What an observer *does* see is the fallout: a leg impaired hard
825 /// enough produces ordinary session and stream events — resets, parse
826 /// errors, [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
827 /// — with no marker distinguishing them from the same failures arriving
828 /// from a real network. That is the honest position: the proxy cannot
829 /// tell either.
830 #[cfg(feature = "impair")]
831 pub fn set_impair(&self, leg: Leg, p: quinn_netem::ImpairProfile) -> Result<(), ControlError> {
832 let handle = self.plane.impair_handle(leg).ok_or(ControlError::Unsupported {
833 what: "a datagram impairment",
834 leg,
835 transport: ControlPlane::NO_SOCKET,
836 })?;
837 handle.arm(p).map_err(|source| ControlError::Impairment { leg, source })
838 }
839
840 /// Remove the datagram impairment from one leg's socket.
841 ///
842 /// Effective from the next datagram that leg passes, exactly as
843 /// [`ProxyControl::set_impair`] is, and idempotent: clearing a leg that
844 /// is not impaired is not an error and not a state change. The other leg
845 /// keeps whatever it has.
846 ///
847 /// The counters and the decision log are **not** cleared. A scenario
848 /// that armed an impairment, cleared it and then read what it had done
849 /// would otherwise find nothing, with no counter anywhere saying a phase
850 /// had been discarded.
851 ///
852 /// A leg this proxy holds no handle for has nothing to clear and nothing
853 /// happens. Unlike `set_impair` there is no refusal to give — the
854 /// signature returns nothing — and none is owed: the request was to have
855 /// no impairment on that leg, and that is exactly the state it is in.
856 /// A caller finding out whether the proxy can impair a leg at all asks
857 /// `set_impair`.
858 ///
859 /// # No observer event
860 ///
861 /// For the reasons `set_impair` gives, all of which apply unchanged: no
862 /// session to name, a synchronous answer the caller already has, and a
863 /// consequence — datagrams no longer being interfered with — that is
864 /// invisible from above QUIC by construction. Disarming is even less
865 /// reportable than arming, because what it produces is the *absence* of
866 /// a loss, and there is no event for a packet that was not dropped.
867 #[cfg(feature = "impair")]
868 pub fn clear_impair(&self, leg: Leg) {
869 if let Some(handle) = self.plane.impair_handle(leg) {
870 handle.disarm();
871 }
872 }
873
874 /// End one session, giving its egress queues a bounded window to flush
875 /// first, and close both of its legs with `code` and `reason`.
876 ///
877 /// Returns as soon as the request has been taken. The drain and the
878 /// close happen in the session, which is the only place that can see
879 /// them through — a method that waited would have to hold the caller
880 /// for as long as the drain took, and the drain is bounded precisely so
881 /// that nobody has to.
882 ///
883 /// # The window, and what happens at the end of it
884 ///
885 /// The window is
886 /// [`EgressConfig::drain_timeout`](crate::action::EgressConfig::drain_timeout)
887 /// from the session's configuration, 100 ms by default. Within it, the
888 /// session keeps running exactly as it was: units come off the
889 /// per-stream queues at their release times, a shaped class keeps
890 /// paying its bucket, and a hook that deferred something still gets it
891 /// written. The window ends early — and this is the common case — the
892 /// moment the session has nothing queued anywhere.
893 ///
894 /// When it ends because the timeout expired, whatever is still queued
895 /// is **abandoned and reported**, once per stream, as
896 /// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown).
897 /// It is not flushed first. A flush at that point would hand the bytes
898 /// to a connection that is about to send `CONNECTION_CLOSE`, which
899 /// discards whatever it had buffered — so the bytes would be neither
900 /// confirmably delivered nor confirmably lost, and no count of them
901 /// would add up. Abandoning them keeps the arithmetic exact: what the
902 /// peer received plus what the impairments name is what was queued when
903 /// this was called.
904 ///
905 /// Either way the session then closes with `code` and `reason`. That
906 /// part is unconditional for the call that was accepted — a drain that
907 /// ran out of time changes what reached the peer, never what the close
908 /// says. A call that is *refused* starts nothing at all; see **Errors**,
909 /// which covers the one refusal a caller can produce deliberately.
910 ///
911 /// # Control streams are not repaired
912 ///
913 /// Nothing is synthesized on a control stream to tidy up the close. If
914 /// a control message was half-written when the window closed, the peer
915 /// gets a truncated message and the session reports
916 /// [`ImpairmentKind::ControlStreamTruncated`](crate::event::ImpairmentKind::ControlStreamTruncated).
917 /// Completing the message would mean the proxy inventing control-stream
918 /// bytes that neither peer wrote.
919 ///
920 /// # Errors
921 ///
922 /// [`ControlError::SessionEnded`] when the session has already ended or
923 /// is already ending — the ordinary race, since a session may end
924 /// between listing it and closing it — and
925 /// [`ControlError::NoSuchSession`] for an id this proxy never ran.
926 ///
927 /// "Already ending" covers one case that is not a race at all, and a
928 /// caller can produce it deliberately: **a second call while the first
929 /// call's drain window is still open**. The first close fixed the code
930 /// and the reason and nothing revises them, so a second call's pair
931 /// would reach neither peer. It is refused here rather than accepted,
932 /// because the only alternative is to take a request and drop it — the
933 /// session's command task is inside the first drain and cancels when it
934 /// comes out, so it never returns for a second. A caller that wants a
935 /// different code has to ask before the first close, not after it.
936 ///
937 /// One thing can survive a refusal, and only in the direction that
938 /// helps: a call that got as far as fixing the pair and then found the
939 /// session unreachable still closes it with what was asked for. So a
940 /// caller that sees this error was either late for everything — the two
941 /// refusals above — or late only for the drain.
942 ///
943 /// # What an observer sees
944 /// This is the one verb here that produces events, and it produces them
945 /// through the session rather than from this call — after the drain, which
946 /// is what makes them a record of what happened rather than of what was
947 /// asked for:
948 ///
949 /// * one
950 /// [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
951 /// whose `reason` begins **`*control plane closed the session*`** and
952 /// quotes `code` and `reason`. That wording is the point: a hook's
953 /// `Action::CloseSession` reaches the same latch and reports `*hook
954 /// closed the session*`, and for a while both said the latter, so an
955 /// operator ending a session was recorded as the scenario under test
956 /// ending it.
957 /// * one
958 /// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown)
959 /// per stream the window ran out on, and none at all for the ordinary
960 /// case where everything drained.
961 /// * at most one
962 /// [`ImpairmentKind::ControlStreamTruncated`](crate::event::ImpairmentKind::ControlStreamTruncated)
963 /// per control direction that was mid-message when the window closed.
964 ///
965 /// Nothing is emitted at the moment the request is *accepted*. The
966 /// return value is that answer, and an event that duplicated it would be
967 /// the only event in this enum an observer could receive for something
968 /// that had not happened yet.
969 pub fn close_session(
970 &self,
971 id: SessionId,
972 code: u32,
973 reason: &[u8],
974 ) -> Result<(), ControlError> {
975 let handle = self.plane.reach(id)?;
976 // Recorded here rather than in the session task so that the pair is
977 // fixed the instant the request is accepted. A session that is torn
978 // down by its peer half a millisecond later still closes with the
979 // code that was asked for, which is what makes this method's
980 // promise independent of how long the session survives it.
981 //
982 // A *losing* record is a session that some other close already
983 // owns — a second call inside the first one's drain window, or a
984 // hook's `Action::CloseSession` that landed between the lookup above
985 // and this line. The first writer keeps the pair, so this call's
986 // code and reason are going nowhere, and the request is refused
987 // instead of handed over. Handing it over would be this crate's
988 // cardinal failure in miniature: a close accepted, reported as
989 // applied, and reaching neither peer.
990 if !handle.closer.record(code, Bytes::copy_from_slice(reason)) {
991 return Err(ControlError::SessionEnded(id));
992 }
993 handle.request(id, SessionCommand::Close { drain: handle.egress.drain_timeout })
994 }
995
996 /// Reset one live forwarded stream, immediately.
997 ///
998 /// `stream` names a stream this session is forwarding; the key is the
999 /// one carried on the events and hook contexts for that stream. `code`
1000 /// becomes the `RESET_STREAM` application error code on the
1001 /// destination, and the source is stopped with the same code, so both
1002 /// peers learn the stream was abandoned rather than finished.
1003 ///
1004 /// # Bytes already handed to the transport are gone
1005 ///
1006 /// A reset abandons the destination stream. Whatever the proxy had
1007 /// written but the transport had not yet acknowledged goes with it —
1008 /// QUIC does not retransmit data on a stream that has been reset — and
1009 /// so does everything this stream still had queued. That is what a
1010 /// reset *is*, and it is why the method exists, but it means the peer's
1011 /// view of this stream ends at an arbitrary byte and no count of what
1012 /// it received is predictable from what was sent.
1013 ///
1014 /// # Errors
1015 ///
1016 /// [`ControlError::NoSuchStream`] when no stream with that key is live
1017 /// — which is the same answer for a key that never existed and for one
1018 /// whose stream has already ended, because a stream's registration is
1019 /// removed as it ends and nothing is left to tell them apart. Plus the
1020 /// two session-level refusals.
1021 ///
1022 /// # One case where `Ok(())` is delivery rather than a reset
1023 ///
1024 /// The request is handed to the task that owns the stream's write half
1025 /// and is acted on the next time that task comes round its own loop,
1026 /// which is immediately in every case but one. A *control* direction on
1027 /// the forward-first pipe stops reading its request channel once it is
1028 /// holding [`COMMAND_QUEUE_DEPTH`](self) injections it has not been able
1029 /// to place — the backpressure that makes a further
1030 /// [`ProxyControl::inject_control`] answer
1031 /// [`ControlError::SessionEnded`] rather than queue without limit — and a
1032 /// reset handed over in that state waits until an injection can be
1033 /// written, which that method's own documentation says can be never.
1034 /// Below that depth, and on every data stream, the reset is served at
1035 /// once.
1036 ///
1037 /// # No observer event, and neither existing one may be borrowed
1038 ///
1039 /// The stream's task performs the reset and emits nothing.
1040 /// [`ProxyEvent::StreamReset`](crate::event::ProxyEvent::StreamReset)
1041 /// means a teardown this proxy *observed* a peer perform, and
1042 /// [`ProxyEvent::ActionApplied`](crate::event::ProxyEvent::ActionApplied)
1043 /// means a hook asked for one at a named site. Reusing either would make
1044 /// it ambiguous for every reader that already relies on it — an observer
1045 /// counting peer resets would start counting the operator's, with
1046 /// nothing in the payload to separate them — and this crate does not
1047 /// widen the meaning of a shipped event to save adding one.
1048 ///
1049 /// A variant of its own was considered and declined: it would carry
1050 /// nothing the caller does not already hold. The session, the stream and
1051 /// the code came from this call, and the answer to "did it happen" is
1052 /// the return value. What is worth observing is the *consequence*, and
1053 /// that is observed where consequences are — at the peer, as a
1054 /// `RESET_STREAM` carrying `code`, which is what this crate's own gate
1055 /// for the method checks.
1056 ///
1057 /// Anything the stream still had queued goes with it and is **not**
1058 /// reported as
1059 /// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown).
1060 /// That report is about a *teardown* losing bytes it was trying to
1061 /// deliver; here the caller asked for the destination to be abandoned,
1062 /// and the bytes going with it is what the word means rather than a
1063 /// reduction in what the proxy could do.
1064 pub fn reset_stream(
1065 &self,
1066 id: SessionId,
1067 stream: StreamKey,
1068 code: u64,
1069 ) -> Result<(), ControlError> {
1070 let handle = self.plane.reach(id)?;
1071 let inbox =
1072 handle.streams.inbox_for(stream).ok_or(ControlError::NoSuchStream { id, stream })?;
1073 match inbox.try_send(StreamCommand::Reset { code }) {
1074 Ok(()) => Ok(()),
1075 // The task ended between the lookup and the send, so its
1076 // receiver is gone. Indistinguishable, to a caller, from a key
1077 // that was already retired — and reported the same way.
1078 Err(mpsc::error::TrySendError::Closed(_)) => {
1079 Err(ControlError::NoSuchStream { id, stream })
1080 }
1081 Err(mpsc::error::TrySendError::Full(_)) => Err(ControlError::SessionEnded(id)),
1082 }
1083 }
1084
1085 /// Put a control message on one leg of a live session, in sequence.
1086 ///
1087 /// `leg` names whose decoder the message is for:
1088 /// [`Leg::Client`] writes toward the client, [`Leg::Upstream`] toward
1089 /// the relay. The bytes are written onto the **session's existing
1090 /// control stream**, interleaved with the messages already flowing on
1091 /// it, at a point the receiving decoder accepts.
1092 ///
1093 /// # What "in sequence" costs, and why it is not a fresh stream
1094 ///
1095 /// A new stream would be far simpler and would look like working code:
1096 /// the write succeeds, the bytes arrive. The peer reads them as
1097 /// whatever a new stream of that kind is — a data stream header on a
1098 /// fresh unidirectional stream, a request on a fresh bidirectional one
1099 /// — and the message is never seen as a control message at all. On the
1100 /// drafts that carry the control plane on unidirectional streams it
1101 /// would be worse than useless: a second stream announcing itself as a
1102 /// control stream is a second SETUP, which a peer is entitled to close
1103 /// the session over. So the write goes to the task that owns the
1104 /// control stream's write half, and lands between two forwarded
1105 /// messages rather than inside one — a control stream is a single
1106 /// framed byte sequence, and bytes spliced into the middle of a
1107 /// message's payload desynchronize the peer's decoder for the rest of
1108 /// the session.
1109 ///
1110 /// Because of that, a message injected while one is mid-flight is held
1111 /// until the message in flight has been written. That wait is bounded
1112 /// by the peer finishing the write it started, not by anything this
1113 /// proxy does.
1114 ///
1115 /// # A held message can wait forever, and this returns `Ok(())` anyway
1116 /// The call returns as soon as the request has been taken, before any write
1117 /// is attempted — it has to, or a caller would be held for as long as the
1118 /// peer took. So `Ok(())` means *accepted for writing*, and on the
1119 /// forward-first control pipe — the one a session takes unless a hook
1120 /// declared `Interest::CONTROL` — there are two ways for the write never to
1121 /// happen:
1122 /// * **the direction goes quiet mid-message.** That pipe forwards read
1123 /// chunks and finds the boundaries in the bytes it is forwarding, so a
1124 /// direction whose peer stopped writing part-way through a message never
1125 /// reaches one. The injection waits for a peer that may never write
1126 /// again.
1127 /// * **the proxy has lost the framing.** The boundary walk is driven by the
1128 /// session's draft, which for the moq-00 cohort (drafts 07-14) is a
1129 /// configured guess until a SETUP is peeked. A wrong guess makes the
1130 /// lengths wrong; rather than place a message by guesswork the walk
1131 /// latches to *the boundaries are unknown*, and from then on nothing is
1132 /// injected on that direction at all. That is deliberate and it is the
1133 /// better of the two outcomes — a misplaced injection desynchronizes the
1134 /// peer's decoder for the rest of the session, while one that never
1135 /// arrives leaves every other message intact.
1136 ///
1137 /// A message still held when the session ends is discarded, and nothing
1138 /// reports it: no event, no impairment, no error. A caller that needs to
1139 /// know an injection landed observes it at the peer, which is what this
1140 /// crate's own gate for the method does.
1141 ///
1142 /// That silence is the weakest point on this page, and it is written
1143 /// down rather than smoothed over. An accepted request that never
1144 /// reaches the wire is exactly the failure this crate refuses
1145 /// everywhere else, and the only thing standing in for a report is the
1146 /// paragraph above. An impairment for it was considered and not added:
1147 /// the held messages live in a local deque of the control pipe, the
1148 /// pipe returns from seven places, and a report wired into some of them
1149 /// would be a promise that is kept for some teardowns and not others —
1150 /// which is worse than no promise, because a scenario would then read
1151 /// the absence of the event as delivery. The report that would be worth
1152 /// having has to be raised by something whose completeness is
1153 /// structural, the way `SessionGuard` is for the session census, and
1154 /// that is not a wiring change.
1155 ///
1156 /// A hook declaring `Interest::CONTROL` puts the stream on the pipe that
1157 /// decodes each message before forwarding it, where every return to the
1158 /// select is a boundary by construction and neither case above exists.
1159 ///
1160 /// # `bytes` must already be framed, and is not checked
1161 ///
1162 /// Pass a complete, framed control message for the session's draft —
1163 /// what `AnyControlMessage::encode` produces, which is the type varint,
1164 /// the length field, and the payload. The proxy writes it verbatim: it
1165 /// does not frame it, does not decode it, and does not know what it
1166 /// says. A payload passed without its framing is the second way to get
1167 /// this wrong that still looks like working code — the write succeeds
1168 /// and the peer's decoder reads the first bytes of the payload as a
1169 /// message type and length, and is lost from there on.
1170 /// Nothing is validated because there is nothing to answer with: the
1171 /// refusals this method can give are about *reaching* a session, and
1172 /// inventing a *those bytes were not a message* refusal would mean decoding
1173 /// every injection on the session's forwarding path.
1174 ///
1175 /// # Which stream that is, per draft
1176 ///
1177 /// Two topologies, and the session picks between them from its draft.
1178 /// Through draft 16 the control plane is one client-initiated
1179 /// bidirectional stream and `leg` names which of its two directions to
1180 /// write on. From draft 17 it is a **pair of unidirectional streams**,
1181 /// one opened by each peer, and bidirectional streams carry requests
1182 /// instead — draft-17 Section 3.3. There `leg` names which of the two
1183 /// unidirectional control streams to write on, and a request stream is
1184 /// never a candidate: the session routes an injection to the control
1185 /// leg's channel, which only a control direction's pipe ever serves.
1186 ///
1187 /// The `leg` mapping is the same on both: the leg is the peer whose
1188 /// decoder reads what is written. [`Leg::Upstream`] puts the message in
1189 /// front of the relay, [`Leg::Client`] in front of the client.
1190 ///
1191 /// # Errors
1192 ///
1193 /// The two session-level refusals. A session whose control stream has
1194 /// not been established yet is **not** an error: the message waits and
1195 /// is written as soon as there is a stream to write it on. On drafts 17
1196 /// and later that wait covers a little more ground — the control stream
1197 /// is identified from the first varint of a unidirectional stream, so a
1198 /// session whose peer has not opened one yet has no control stream in
1199 /// that direction and the message waits for it exactly as it waits for
1200 /// a message boundary.
1201 ///
1202 /// # What an observer sees
1203 ///
1204 /// Nothing for the injection itself — no event on acceptance, none on
1205 /// the write, none on the discard described above. The bytes are written
1206 /// verbatim and are never decoded here, so there is no message to put on
1207 /// a
1208 /// [`ProxyEvent::ControlMessage`](crate::event::ProxyEvent::ControlMessage)
1209 /// and no honest way to synthesize one; and the peer's own reaction, if
1210 /// it has one, arrives as ordinary forwarded traffic.
1211 pub fn inject_control(
1212 &self,
1213 id: SessionId,
1214 leg: Leg,
1215 bytes: Vec<u8>,
1216 ) -> Result<(), ControlError> {
1217 let handle = self.plane.reach(id)?;
1218 handle
1219 .control_inbox(leg)
1220 .try_send(StreamCommand::Inject { bytes: Bytes::from(bytes) })
1221 .map_err(|_| ControlError::SessionEnded(id))
1222 }
1223}
1224
1225/// The state a [`ProxyControl`] reads, owned by the proxy and shared with
1226/// every handle it hands out.
1227///
1228/// One per [`TransparentProxy`](crate::proxy::TransparentProxy), never
1229/// process-global: session ids are minted from a counter on the proxy and
1230/// start again at 1 for each one, so two proxies in a process would collide
1231/// on every id in a shared table.
1232pub(crate) struct ControlPlane {
1233 /// The client-facing leg: the listener once it exists, and the transport
1234 /// parameters a live request has installed on it.
1235 ///
1236 /// One lock over both because the two have to move together. A request
1237 /// arriving while `run()` is between binding the endpoint and publishing
1238 /// it must not be dropped: it stores the parameters and finds no
1239 /// listener, and the publish then installs them. Split across two locks
1240 /// there is a window in which a request stores its parameters after the
1241 /// publish has read them and before the listener is visible, and the
1242 /// setting is accepted, reported as applied, and reaches nothing.
1243 client: Mutex<ClientLeg>,
1244 /// The transport parameters a live request has set for the relay leg, or
1245 /// `None` while the proxy's own template is what every session dials
1246 /// with.
1247 upstream_transport: Mutex<Option<TransportProfile>>,
1248 /// The two legs' fixed facts, from the configuration the proxy was built
1249 /// with.
1250 legs: LegSetup,
1251 /// This proxy's shaping profile and its pacing switch.
1252 shape: ShapeControl,
1253 /// What every session this proxy accepts reports its shaping figures
1254 /// into, on top of its own.
1255 ///
1256 /// Held here rather than on the [`TransparentProxy`] because this is
1257 /// what a session is already handed: `attach_control` is the one call
1258 /// that reaches every accepted session and no directly-driven one, which
1259 /// is exactly the set whose traffic belongs in a proxy-wide total.
1260 ///
1261 /// Constructed with the plane and never replaced, so it outlives every
1262 /// session and a total taken from it never falls when one ends —
1263 /// unlike [`Self::sessions`], whose entries are released by a `Drop`.
1264 stats: Arc<ProxyRecorder>,
1265 /// The two legs' impairment handles, indexed by [`Self::leg_index`].
1266 ///
1267 /// `None` for a leg whose socket this proxy did not wrap, which is every
1268 /// leg unless the caller handed both halves over — see
1269 /// [`TransparentProxy::set_impaired_socket`](crate::proxy::TransparentProxy::set_impaired_socket).
1270 #[cfg(feature = "impair")]
1271 impair: Mutex<[Option<quinn_netem::ImpairHandle>; 2]>,
1272 /// Every session that is running right now.
1273 sessions: Mutex<HashMap<SessionId, SessionHandle>>,
1274 /// The highest id ever registered here.
1275 ///
1276 /// The only trace a finished session leaves, and it exists to separate
1277 /// [`ControlError::SessionEnded`] from [`ControlError::NoSuchSession`].
1278 /// Ids are minted monotonically from a counter on the proxy, so an id
1279 /// at or below this one has run; anything above it has not. A set of
1280 /// retired ids would answer the same question and would grow for the
1281 /// life of the proxy.
1282 ///
1283 /// `0` before any session registers, and no session is ever `SessionId(0)`.
1284 high_water: AtomicU64,
1285}
1286
1287// Hand-written rather than derived because `Listener` has no `Debug`, and
1288// giving it one would widen a public type's surface for a diagnostic nobody
1289// reads. Nothing here prints the sessions either: the map is behind a lock
1290// that a formatting call has no business taking, and `sessions()` is the
1291// supported way to ask.
1292impl std::fmt::Debug for ControlPlane {
1293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1294 f.debug_struct("ControlPlane").finish_non_exhaustive()
1295 }
1296}
1297
1298impl ControlPlane {
1299 /// An empty control plane: nothing bound, no sessions.
1300 pub(crate) fn new(legs: LegSetup) -> Arc<Self> {
1301 Arc::new(Self {
1302 client: Mutex::new(ClientLeg { listener: None, transport: None }),
1303 upstream_transport: Mutex::new(None),
1304 legs,
1305 shape: ShapeControl::new(),
1306 stats: Arc::new(ProxyRecorder::new()),
1307 #[cfg(feature = "impair")]
1308 impair: Mutex::new([None, None]),
1309 sessions: Mutex::new(HashMap::new()),
1310 high_water: AtomicU64::new(0),
1311 })
1312 }
1313
1314 /// Publish `listener` as this proxy's bound endpoint, and hand back the
1315 /// guard that un-publishes it.
1316 ///
1317 /// A guard rather than a matching `clear` call because the accept loop
1318 /// leaves by three routes — cancellation, a fatal listener error
1319 /// propagated with `?`, and the whole future being dropped by whoever
1320 /// spawned it — and only one of them is a place a call could be
1321 /// written.
1322 ///
1323 /// Any transport parameters a request set before there was a listener
1324 /// are installed here, under the same lock the request took, which is
1325 /// what makes a request that arrives during the bind land on the
1326 /// endpoint rather than on nothing.
1327 pub(crate) fn publish_listener(self: &Arc<Self>, listener: Arc<Listener>) -> BoundGuard {
1328 let mut client = self.client.lock().expect("control plane");
1329 if let Some(transport) = &client.transport {
1330 listener.set_transport(Arc::clone(transport));
1331 }
1332 client.listener = Some(listener);
1333 drop(client);
1334 BoundGuard { plane: Arc::clone(self) }
1335 }
1336
1337 /// The bound listener, or `None` before there is one.
1338 fn bound(&self) -> Option<Arc<Listener>> {
1339 self.client.lock().expect("control plane").listener.clone()
1340 }
1341
1342 /// Forget the bound listener.
1343 fn unbind(&self) {
1344 self.client.lock().expect("control plane").listener = None;
1345 }
1346
1347 /// The transport parameters the client leg should bind with, or `None`
1348 /// when no request has set any and the proxy's template stands.
1349 pub(crate) fn client_transport(&self) -> Option<Arc<quinn::TransportConfig>> {
1350 self.client.lock().expect("control plane").transport.clone()
1351 }
1352
1353 /// Install `transport` on the client leg, now if there is an endpoint
1354 /// and at bind time if there is not.
1355 fn set_client_transport(&self, transport: Arc<quinn::TransportConfig>) {
1356 let mut client = self.client.lock().expect("control plane");
1357 client.transport = Some(Arc::clone(&transport));
1358 if let Some(listener) = &client.listener {
1359 listener.set_transport(transport);
1360 }
1361 }
1362
1363 /// The profile every session this proxy accepts from now on dials the
1364 /// relay with, or `None` when the proxy's template stands.
1365 pub(crate) fn upstream_transport(&self) -> Option<TransportProfile> {
1366 self.upstream_transport.lock().expect("control plane").clone()
1367 }
1368
1369 /// This proxy's shaping profile and pacing switch, for a session to
1370 /// build its scheduler from and to watch.
1371 pub(crate) fn shape(&self) -> &ShapeControl {
1372 &self.shape
1373 }
1374
1375 /// The proxy-wide shaping counters, for a session to report into.
1376 ///
1377 /// Handed out as an `Arc` clone rather than a borrow because the session
1378 /// keeps it: its recorder forwards into this for the session's whole
1379 /// life, which outlives any borrow of the plane a call could hold.
1380 pub(crate) fn stats_recorder(&self) -> Arc<ProxyRecorder> {
1381 Arc::clone(&self.stats)
1382 }
1383
1384 /// Turn `profile` into the config `leg` would install, refusing exactly
1385 /// what a configured profile on that leg would be refused for.
1386 ///
1387 /// Built through the leg's own [`TransportInstaller`] when it has one,
1388 /// so a caller who supplied an installer to start with gets the same
1389 /// step here — a live request that quietly used the default installer
1390 /// instead would produce a connection built differently from every one
1391 /// the proxy made before it, with nothing saying so.
1392 fn build_transport(
1393 &self,
1394 leg: Leg,
1395 profile: &TransportProfile,
1396 ) -> Result<Arc<quinn::TransportConfig>, ControlError> {
1397 let installer = match leg {
1398 Leg::Client => &self.legs.client_installer,
1399 Leg::Upstream => &self.legs.upstream_installer,
1400 };
1401 match installer {
1402 Some(installer) => installer.build(profile),
1403 None => DefaultInstaller.build(profile),
1404 }
1405 .map(Arc::new)
1406 .map_err(ControlError::Profile)
1407 }
1408
1409 /// Which slot of the per-leg arrays `leg` owns.
1410 #[cfg(feature = "impair")]
1411 fn leg_index(leg: Leg) -> usize {
1412 match leg {
1413 Leg::Client => 0,
1414 Leg::Upstream => 1,
1415 }
1416 }
1417
1418 /// The impairment handle for `leg`, or `None` when this proxy was never
1419 /// given one.
1420 #[cfg(feature = "impair")]
1421 fn impair_handle(&self, leg: Leg) -> Option<quinn_netem::ImpairHandle> {
1422 self.impair.lock().expect("control plane")[Self::leg_index(leg)].clone()
1423 }
1424
1425 /// Record the handle that arms `leg`'s socket.
1426 #[cfg(feature = "impair")]
1427 pub(crate) fn set_impair_handle(&self, leg: Leg, handle: quinn_netem::ImpairHandle) {
1428 self.impair.lock().expect("control plane")[Self::leg_index(leg)] = Some(handle);
1429 }
1430
1431 /// What a leg with no impairment handle is told, and how to fix it.
1432 ///
1433 /// Carried in `Unsupported::transport` because that is the only field
1434 /// left to say it in and a caller who cannot act on the refusal is worse
1435 /// off than one who was never given the verb.
1436 #[cfg(feature = "impair")]
1437 const NO_SOCKET: &'static str = "a socket this proxy did not wrap — hand that leg's socket \
1438 and the ImpairHandle it was wrapped with to \
1439 TransparentProxy::set_impaired_socket before run()";
1440}
1441
1442/// The client-facing leg's two mutable facts, under one lock.
1443struct ClientLeg {
1444 /// The listener, from the moment `run()` binds it until `run()` returns.
1445 listener: Option<Arc<Listener>>,
1446 /// The transport parameters a live request installed, kept so that a
1447 /// request that arrives before the bind is not lost.
1448 transport: Option<Arc<quinn::TransportConfig>>,
1449}
1450
1451/// What the control plane knows about the two legs from the configuration
1452/// the proxy was built with, and cannot learn any other way.
1453///
1454/// Copied out of the proxy's templates when the plane is built rather than
1455/// read back from them on demand, because a handle is reachable from tasks
1456/// that do not hold the proxy.
1457#[derive(Default)]
1458pub(crate) struct LegSetup {
1459 /// How a client-leg [`TransportProfile`] becomes the config the listener
1460 /// installs.
1461 pub(crate) client_installer: Option<Arc<dyn TransportInstaller>>,
1462 /// The same for the relay leg.
1463 pub(crate) upstream_installer: Option<Arc<dyn TransportInstaller>>,
1464 /// Whether the relay leg is a WebTransport upstream, which cannot take
1465 /// transport parameters at all.
1466 pub(crate) upstream_webtransport: bool,
1467}
1468
1469/// This proxy's shaping profile, and whether it paces.
1470///
1471/// One per proxy, watched by every session it accepts. The generation is
1472/// what lets a session notice a change without comparing profiles: a session
1473/// caches the generation its scheduler was built at, and a mismatch is the
1474/// signal to look.
1475pub(crate) struct ShapeControl {
1476 /// The profile a live request installed, or `None` while the proxy's own
1477 /// template is what every session shapes with.
1478 profile: Mutex<Option<ShapeProfile>>,
1479 /// Bumped, under the lock above, every time the profile is replaced.
1480 generation: AtomicU64,
1481 /// Whether the schedulers built from this pace at all. Behind its own
1482 /// `Arc` because every scheduler holds a clone of it.
1483 enabled: Arc<AtomicBool>,
1484}
1485
1486impl ShapeControl {
1487 /// A control with no profile of its own and pacing switched on, which is
1488 /// the state that leaves a proxy behaving exactly as its configuration
1489 /// says.
1490 fn new() -> Self {
1491 Self {
1492 profile: Mutex::new(None),
1493 generation: AtomicU64::new(0),
1494 enabled: Arc::new(AtomicBool::new(true)),
1495 }
1496 }
1497
1498 /// Replace the profile and move the generation on.
1499 fn set(&self, profile: ShapeProfile) {
1500 let mut held = self.profile.lock().expect("shape control");
1501 *held = Some(profile);
1502 self.generation.fetch_add(1, Ordering::AcqRel);
1503 }
1504
1505 /// The current generation, without cloning a profile.
1506 ///
1507 /// What a session checks per stream; the clone below is paid only when
1508 /// this has moved.
1509 pub(crate) fn generation(&self) -> u64 {
1510 self.generation.load(Ordering::Acquire)
1511 }
1512
1513 /// The generation and the profile it names, read together.
1514 ///
1515 /// Read under the lock the setter writes under, so the pair cannot
1516 /// straddle two calls to [`Self::set`] — a session that cached a
1517 /// generation from one profile and a profile from another would stop
1518 /// noticing the newer of the two.
1519 pub(crate) fn snapshot(&self) -> (u64, Option<ShapeProfile>) {
1520 let held = self.profile.lock().expect("shape control");
1521 (self.generation.load(Ordering::Acquire), held.clone())
1522 }
1523
1524 /// The switch every scheduler built from this reads.
1525 pub(crate) fn switch(&self) -> Arc<AtomicBool> {
1526 Arc::clone(&self.enabled)
1527 }
1528
1529 /// Turn pacing on or off for every session at once.
1530 fn set_enabled(&self, on: bool) {
1531 self.enabled.store(on, Ordering::Release);
1532 }
1533}
1534
1535impl ControlPlane {
1536 /// Register `id` as live, and hand back the guard that ends it.
1537 ///
1538 /// See this module's own documentation for why the removal is a `Drop`
1539 /// and not a call on each of the session's termination paths.
1540 pub(crate) fn register(self: &Arc<Self>, id: SessionId, handle: SessionHandle) -> SessionGuard {
1541 self.sessions.lock().expect("control plane").insert(id, handle);
1542 self.high_water.fetch_max(id.0, Ordering::AcqRel);
1543 SessionGuard { plane: Arc::clone(self), id }
1544 }
1545
1546 /// Retire `id`. Idempotent.
1547 fn end(&self, id: SessionId) {
1548 self.sessions.lock().expect("control plane").remove(&id);
1549 }
1550
1551 /// Every live id, ascending.
1552 fn live(&self) -> Vec<SessionId> {
1553 let mut ids: Vec<SessionId> =
1554 self.sessions.lock().expect("control plane").keys().copied().collect();
1555 ids.sort_by_key(|id| id.0);
1556 ids
1557 }
1558
1559 /// The handle for one live session, or `None` when it is not registered.
1560 ///
1561 /// The only correct way to read the map: a caller that held the lock
1562 /// itself could still be holding it while it talked to the session.
1563 fn session(&self, id: SessionId) -> Option<SessionHandle> {
1564 self.sessions.lock().expect("control plane").get(&id).cloned()
1565 }
1566
1567 /// The handle to act on `id` through, or the refusal that says why
1568 /// there is none.
1569 /// Three answers, and the third is the one worth stating: a session that is
1570 /// registered but whose cancellation token has already fired is refused as
1571 /// ended rather than acted on. It is going down and its tasks are
1572 /// unwinding, so a request accepted there would be taken and then never
1573 /// served — which is exactly the *accepts a request and discards it*
1574 /// outcome this module refuses to have.
1575 fn reach(&self, id: SessionId) -> Result<SessionHandle, ControlError> {
1576 match self.session(id) {
1577 Some(handle) if !handle.cancel.is_cancelled() => Ok(handle),
1578 Some(_) => Err(ControlError::SessionEnded(id)),
1579 // `id.0 != 0` because the proxy's counter starts at 1, so
1580 // `SessionId(0)` names nothing however many sessions have run;
1581 // without it a proxy that has never registered anything would
1582 // report that id as ended.
1583 None if id.0 != 0 && id.0 <= self.high_water.load(Ordering::Acquire) => {
1584 Err(ControlError::SessionEnded(id))
1585 }
1586 None => Err(ControlError::NoSuchSession(id)),
1587 }
1588 }
1589}
1590
1591/// The published bound listener's registration. Un-publishes on drop.
1592pub(crate) struct BoundGuard {
1593 plane: Arc<ControlPlane>,
1594}
1595
1596impl Drop for BoundGuard {
1597 fn drop(&mut self) {
1598 self.plane.unbind();
1599 }
1600}
1601
1602/// One live session's registration. Ends it on drop.
1603///
1604/// Held in the frame of the function that runs the session, so the
1605/// registration lasts exactly as long as the session does — including when
1606/// that function returns early, and including when its future is dropped
1607/// wholesale rather than run to completion.
1608pub(crate) struct SessionGuard {
1609 plane: Arc<ControlPlane>,
1610 id: SessionId,
1611}
1612
1613impl Drop for SessionGuard {
1614 fn drop(&mut self) {
1615 self.plane.end(self.id);
1616 }
1617}
1618
1619/// What the control plane keeps for one live session.
1620///
1621/// Everything here is either a handle onto shared state the session set up
1622/// before it began forwarding, or a way to reach a task it spawned. Nothing
1623/// here is a `Transport`: the two connections are owned by the forwarding
1624/// scope, and putting them in a table scanned by every list call would keep
1625/// them alive past the session that owns them.
1626///
1627/// The entry can therefore be built at the *top* of the session's run
1628/// function, before the upstream relay has even been dialled — which is
1629/// what makes a session reachable during the one operation that can take
1630/// seconds.
1631///
1632/// `Clone` because a request takes a copy out from under the registry lock
1633/// before it does anything with it; holding that lock while talking to a
1634/// session would let one unresponsive session block every other request.
1635#[derive(Debug, Clone)]
1636pub(crate) struct SessionHandle {
1637 /// The session's own token — a child of the proxy's, so cancelling it
1638 /// ends this session and leaves the proxy accepting. Read, not
1639 /// cancelled, by this module: a session that is already going down is
1640 /// refused rather than asked to do more.
1641 cancel: CancellationToken,
1642 /// Where the close code and reason are fixed, and where the session's
1643 /// own teardown reads them back out.
1644 closer: SessionCloser,
1645 /// Every stream this session is forwarding, and each one's inbox.
1646 streams: Arc<StreamRegistry>,
1647 /// The two control-stream directions' inboxes, indexed by the leg whose
1648 /// peer decodes what is written there. See [`Self::control_inbox`].
1649 control: [mpsc::Sender<StreamCommand>; 2],
1650 /// The session's egress knobs, for the drain window a close is given.
1651 egress: EgressConfig,
1652 /// Where a request for the session as a whole is delivered.
1653 commands: mpsc::Sender<SessionCommand>,
1654}
1655
1656impl SessionHandle {
1657 /// The control-stream inbox whose writes `leg`'s peer decodes.
1658 ///
1659 /// The mapping is a half-turn and is worth stating once: the pipe that
1660 /// forwards *from* the client writes *to* the relay, so a message meant
1661 /// for the relay's decoder — [`Leg::Upstream`] — goes to the
1662 /// client-to-proxy direction's inbox, and a message for the client goes
1663 /// to the relay-to-proxy direction's.
1664 fn control_inbox(&self, leg: Leg) -> &mpsc::Sender<StreamCommand> {
1665 match leg {
1666 Leg::Client => &self.control[0],
1667 Leg::Upstream => &self.control[1],
1668 }
1669 }
1670
1671 /// Hand `command` to this session's command task.
1672 fn request(&self, id: SessionId, command: SessionCommand) -> Result<(), ControlError> {
1673 // Both failures are the same answer: the request was not taken.
1674 // `Closed` is a session whose task has stopped, `Full` is one that
1675 // has sixteen outstanding and has served none — see
1676 // `ControlError::SessionEnded`, which says why there is no third.
1677 self.commands.try_send(command).map_err(|_| ControlError::SessionEnded(id))
1678 }
1679}
1680
1681/// A request delivered to one session's command task.
1682///
1683/// Crate-internal and not a mirror of the public surface: the two
1684/// stream-level verbs resolve in the caller and never appear here, and the
1685/// one that does reach a session arrives already validated, so the session
1686/// task never has to decide whether a request was well-formed.
1687pub(crate) enum SessionCommand {
1688 /// Give this session's egress queues `drain` to flush, then end it.
1689 ///
1690 /// Carries no close code, because the code and reason were recorded
1691 /// into the session's closer before this was sent and are read back
1692 /// from there at teardown. Carrying them here as well would be a second
1693 /// source for one fact, and the two could disagree if the session were
1694 /// torn down by its peer between the two steps — which is precisely the
1695 /// case the recording is done first to get right.
1696 Close {
1697 /// How long the queues get before whatever is left is abandoned.
1698 drain: Duration,
1699 },
1700}
1701
1702/// A request delivered to the task that owns one forwarded stream.
1703///
1704/// A channel rather than a shared handle because both operations here need
1705/// `&mut SendStream`, and the stream is moved by value into its forwarding
1706/// task on every path this proxy takes. See
1707/// [`StreamEntry`] for the rest of that
1708/// argument.
1709#[derive(Debug)]
1710pub(crate) enum StreamCommand {
1711 /// Abandon this stream: reset the destination with `code` and stop the
1712 /// source with the same code.
1713 Reset {
1714 /// The application error code both peers are given.
1715 code: u64,
1716 },
1717 /// Write these bytes onto this stream at the next point the receiving
1718 /// decoder accepts one.
1719 ///
1720 /// Only the two control directions ever receive this. A data stream's
1721 /// task has no notion of a message boundary — it forwards chunks — so
1722 /// it could not honour the "in sequence" half of the request, and
1723 /// nothing routes one to it.
1724 Inject {
1725 /// A complete, framed control message, written verbatim.
1726 bytes: Bytes,
1727 },
1728}
1729
1730/// The two ends of one control-stream direction's request channel.
1731///
1732/// Created in the session's run function, before the control stream exists,
1733/// so that the sending half can go into the control-plane registry at the
1734/// same moment as the rest of the session's entry. An injection that
1735/// arrives before the control stream has been established waits in this
1736/// channel rather than being refused — the session is live, and there will
1737/// be a stream.
1738pub(crate) struct ControlLeg {
1739 /// Where a request for this direction is delivered.
1740 pub(crate) inbox: mpsc::Sender<StreamCommand>,
1741 /// What the direction's pipe serves.
1742 pub(crate) requests: mpsc::Receiver<StreamCommand>,
1743}
1744
1745impl ControlLeg {
1746 /// A fresh channel for one control-stream direction.
1747 pub(crate) fn new() -> Self {
1748 let (inbox, requests) = mpsc::channel(COMMAND_QUEUE_DEPTH);
1749 Self { inbox, requests }
1750 }
1751}
1752
1753/// A session's attachment to its proxy's control plane.
1754///
1755/// Built when the proxy constructs the session, which is before the session
1756/// runs and long before it has anything worth reaching. It holds the
1757/// channel's two halves apart until then: the sending half goes into the
1758/// registry when the session registers, and the receiving half is taken out
1759/// once, by the session itself, when it has the context to serve it.
1760///
1761/// A session built directly, rather than by a proxy's accept loop, has no
1762/// attachment at all — it belongs to no proxy, so there is no plane for it
1763/// to register with.
1764pub(crate) struct ControlAttachment {
1765 plane: Arc<ControlPlane>,
1766 commands: mpsc::Sender<SessionCommand>,
1767 inbox: Mutex<Option<mpsc::Receiver<SessionCommand>>>,
1768}
1769
1770impl ControlAttachment {
1771 /// Attach a session to `plane`, minting its command channel.
1772 pub(crate) fn new(plane: Arc<ControlPlane>) -> Self {
1773 let (commands, inbox) = mpsc::channel(COMMAND_QUEUE_DEPTH);
1774 Self { plane, commands, inbox: Mutex::new(Some(inbox)) }
1775 }
1776
1777 /// Register this session as live under `id`, reachable through
1778 /// everything a request might have to touch.
1779 ///
1780 /// The returned guard is the registration; drop it and the id is gone.
1781 ///
1782 /// Every argument is state the session builds before it dials the
1783 /// upstream relay, which is why this can be called at the top of the
1784 /// run function: a session spends its longest single operation
1785 /// connecting, and one that only became reachable afterwards would be
1786 /// unreachable for exactly as long as that took.
1787 pub(crate) fn register(
1788 &self,
1789 id: SessionId,
1790 cancel: CancellationToken,
1791 closer: SessionCloser,
1792 streams: Arc<StreamRegistry>,
1793 control: [mpsc::Sender<StreamCommand>; 2],
1794 egress: EgressConfig,
1795 ) -> SessionGuard {
1796 self.plane.register(
1797 id,
1798 SessionHandle {
1799 cancel,
1800 closer,
1801 streams,
1802 control,
1803 egress,
1804 commands: self.commands.clone(),
1805 },
1806 )
1807 }
1808
1809 /// The plane this session is attached to.
1810 ///
1811 /// Handed out so that the session can build a shaper that watches the
1812 /// proxy's profile. Nothing else the session does needs the plane: the
1813 /// registration goes through [`Self::register`] and the command channel
1814 /// through [`Self::take_inbox`], both of which hand back exactly what
1815 /// they cover.
1816 pub(crate) fn plane(&self) -> Arc<ControlPlane> {
1817 Arc::clone(&self.plane)
1818 }
1819
1820 /// Take the receiving half of the command channel.
1821 ///
1822 /// Answers `Some` exactly once per session. A second caller gets `None`
1823 /// rather than a second receiver, because two tasks draining one inbox
1824 /// would each see half the requests and neither would know it.
1825 pub(crate) fn take_inbox(&self) -> Option<mpsc::Receiver<SessionCommand>> {
1826 self.inbox.lock().expect("control attachment").take()
1827 }
1828}
1829
1830/// A spawned task that is aborted when this value is dropped.
1831///
1832/// Used for a session's command task, which has no natural end of its own:
1833/// it waits on a channel and on a cancellation token, and if the session's
1834/// future is dropped without either firing — which is what happens when
1835/// whoever spawned the session aborts it — the task would otherwise outlive
1836/// the session that owns it, holding the session's context alive with it.
1837pub(crate) struct AbortOnDrop {
1838 task: JoinHandle<()>,
1839}
1840
1841impl AbortOnDrop {
1842 /// Take ownership of `task`.
1843 pub(crate) fn new(task: JoinHandle<()>) -> Self {
1844 Self { task }
1845 }
1846}
1847
1848impl Drop for AbortOnDrop {
1849 fn drop(&mut self) {
1850 self.task.abort();
1851 }
1852}
1853
1854// ── The live-stream registry ────────────────────────────────────────────
1855//
1856// A stream's entry is what a request naming it is routed through, so this
1857// lives beside `StreamCommand` rather than beside the shaping profile it was
1858// first written next to. The move is what keeps `shape` from naming anything
1859// in this module.
1860
1861/// Every forwarded stream that is still live, and the [`Gate`] each one
1862/// releases when it ends.
1863///
1864/// This is what
1865/// [`StreamAction::SerializeAfter`](crate::action::StreamAction::SerializeAfter)
1866/// waits on. It is **always constructed**, independently of whether the
1867/// session has a [`ShapeProfile`]: `SerializeAfter` is gated by
1868/// `Interest::STREAMS`, and the capability table publishes it as an
1869/// unconditional `Yes` at both stream sites, so a registry that only existed
1870/// when a profile was configured would make that published cell a lie. Empty,
1871/// it is one `HashMap` header behind an `Arc` and allocates nothing until a
1872/// stream registers.
1873///
1874/// Engine-internal despite living in a `pub` module: a scenario author names
1875/// a [`StreamKey`], never a registry.
1876///
1877/// # Why a lookup miss is not an error
1878///
1879/// [`Self::gate_for`] answers `None` for a key that never existed **and** for
1880/// one whose stream has already ended, because an entry is removed as it is
1881/// released. Both mean the same thing to a waiter — there is nothing left to
1882/// wait for — and the two are deliberately not distinguished: a serialize
1883/// that resolves immediately is correct in both cases, and the caller reports
1884/// `SerializeTargetUnknown` once so the run says which streams did it.
1885#[derive(Debug)]
1886pub(crate) struct StreamRegistry {
1887 live: Mutex<HashMap<StreamKey, StreamEntry>>,
1888}
1889
1890/// What the registry keeps for one live stream.
1891///
1892/// Two things, and they are here for two different callers. The [`Gate`] is
1893/// what a `SerializeAfter` waits on and has always been the whole of an
1894/// entry. The inbox is how a request that arrives from *outside* the
1895/// session reaches the task that owns the stream's write half.
1896///
1897/// The inbox is a channel rather than a shared handle because
1898/// `SendStream::write_all` and `SendStream::reset` both take `&mut self`
1899/// and the stream is moved by value into its forwarding task on every
1900/// path. Sharing it would mean a lock, and that lock would be held across
1901/// `write_all().await` — on the byte-pump fast path, for as long as the
1902/// destination's flow control took. Handing the task a message instead
1903/// costs one `select!` branch and keeps the write where it already is.
1904#[derive(Debug, Clone)]
1905pub(crate) struct StreamEntry {
1906 /// Released when the stream ends.
1907 gate: Gate,
1908 /// Where a request aimed at this stream is delivered.
1909 inbox: tokio::sync::mpsc::Sender<StreamCommand>,
1910}
1911
1912impl StreamRegistry {
1913 /// An empty registry.
1914 pub(crate) fn new() -> Self {
1915 Self { live: Mutex::new(HashMap::new()) }
1916 }
1917
1918 /// Register `key` as live, and hand back the guard that ends it.
1919 ///
1920 /// The guard is the whole release mechanism, and it is a guard rather
1921 /// than a call at each teardown site on purpose. The gate has to be
1922 /// released on **every** termination path without exception — FIN,
1923 /// mirrored reset, synthesized reset, `STOP_SENDING`, cancellation — and
1924 /// a missed release is not a loud failure but a `max_hold` stall on some
1925 /// *other* stream. An enumerated list is only as complete as the reader;
1926 /// `Drop` is complete by construction, and it additionally covers the
1927 /// paths no enumeration would have listed: a `?` return, a panicking
1928 /// forwarding task, and the task future being dropped wholesale by
1929 /// `JoinSet::shutdown` at session teardown.
1930 /// `inbox` is where a request naming this key is delivered; it is the
1931 /// sending half of a channel whose receiving half the forwarding task
1932 /// holds, so the entry going away and the task stopping are one event.
1933 pub(crate) fn register(
1934 self: &Arc<Self>,
1935 key: StreamKey,
1936 inbox: tokio::sync::mpsc::Sender<StreamCommand>,
1937 ) -> StreamGuard {
1938 self.live
1939 .lock()
1940 .expect("stream registry")
1941 .insert(key, StreamEntry { gate: Gate::new(), inbox });
1942 StreamGuard { registry: Arc::clone(self), key }
1943 }
1944
1945 /// The gate to wait on for `target`, or `None` when there is nothing to
1946 /// wait for — see the type's own doc for why those are one answer.
1947 pub(crate) fn gate_for(&self, target: StreamKey) -> Option<Gate> {
1948 self.live.lock().expect("stream registry").get(&target).map(|e| e.gate.clone())
1949 }
1950
1951 /// Where to deliver a request aimed at `target`, or `None` when no such
1952 /// stream is live.
1953 ///
1954 /// The same "never existed" / "already ended" conflation
1955 /// [`Self::gate_for`] makes, and for the same reason: the entry is
1956 /// removed as the stream ends, so nothing is left to tell the two
1957 /// apart. A caller reports both as "no such stream", which is what a
1958 /// request aimed at either can act on.
1959 pub(crate) fn inbox_for(
1960 &self,
1961 target: StreamKey,
1962 ) -> Option<tokio::sync::mpsc::Sender<StreamCommand>> {
1963 self.live.lock().expect("stream registry").get(&target).map(|e| e.inbox.clone())
1964 }
1965
1966 /// Retire `key`: remove it and release its gate.
1967 ///
1968 /// Idempotent, and the removal is what makes a later `gate_for` on the
1969 /// same key answer `None` rather than handing out an already-released
1970 /// gate that would look live.
1971 fn end(&self, key: StreamKey) {
1972 let entry = self.live.lock().expect("stream registry").remove(&key);
1973 if let Some(entry) = entry {
1974 entry.gate.release();
1975 }
1976 }
1977
1978 /// How many streams are live. Tests only.
1979 #[cfg(test)]
1980 fn len(&self) -> usize {
1981 self.live.lock().expect("stream registry").len()
1982 }
1983}
1984
1985/// One live stream's registration. Ends it on drop.
1986///
1987/// Held by the forwarding task for exactly as long as the stream is
1988/// forwarding; see [`StreamRegistry::register`] for why the release is a
1989/// `Drop` rather than a call on each teardown path.
1990pub(crate) struct StreamGuard {
1991 registry: Arc<StreamRegistry>,
1992 key: StreamKey,
1993}
1994
1995impl Drop for StreamGuard {
1996 fn drop(&mut self) {
1997 self.registry.end(self.key);
1998 }
1999}
2000
2001#[cfg(test)]
2002mod tests {
2003 use super::*;
2004 use crate::types::ProxySide;
2005
2006 fn handle() -> SessionHandle {
2007 let (commands, _inbox) = mpsc::channel(COMMAND_QUEUE_DEPTH);
2008 let cancel = CancellationToken::new();
2009 SessionHandle {
2010 closer: SessionCloser::new(cancel.clone()),
2011 cancel,
2012 streams: Arc::new(StreamRegistry::new()),
2013 control: [ControlLeg::new().inbox, ControlLeg::new().inbox],
2014 egress: EgressConfig::default(),
2015 commands,
2016 }
2017 }
2018
2019 /// A registration lasts exactly as long as its guard.
2020 ///
2021 /// The whole reliability claim of `sessions()` rests on this: no
2022 /// termination path in `session.rs` calls a removal, so if the guard's
2023 /// `Drop` did not remove the entry, nothing would, and the list would
2024 /// grow monotonically for the life of the proxy.
2025 #[test]
2026 fn a_dropped_guard_takes_its_id_out_of_the_list() {
2027 let plane = ControlPlane::new(LegSetup::default());
2028 assert!(plane.live().is_empty());
2029
2030 let first = plane.register(SessionId(1), handle());
2031 let second = plane.register(SessionId(2), handle());
2032 assert_eq!(plane.live(), vec![SessionId(1), SessionId(2)]);
2033
2034 drop(first);
2035 assert_eq!(
2036 plane.live(),
2037 vec![SessionId(2)],
2038 "the id that ended goes, and the one still running stays"
2039 );
2040
2041 drop(second);
2042 assert!(plane.live().is_empty());
2043 }
2044
2045 /// The list comes back ascending however the ids went in.
2046 #[test]
2047 fn the_list_is_sorted_by_id() {
2048 let plane = ControlPlane::new(LegSetup::default());
2049 let _c = plane.register(SessionId(3), handle());
2050 let _a = plane.register(SessionId(1), handle());
2051 let _b = plane.register(SessionId(2), handle());
2052
2053 assert_eq!(plane.live(), vec![SessionId(1), SessionId(2), SessionId(3)]);
2054 }
2055
2056 /// The inbox is handed out once, so two tasks cannot split one session's
2057 /// requests between them.
2058 #[test]
2059 fn the_command_inbox_can_only_be_taken_once() {
2060 let attachment = ControlAttachment::new(ControlPlane::new(LegSetup::default()));
2061 assert!(attachment.take_inbox().is_some());
2062 assert!(attachment.take_inbox().is_none());
2063 }
2064
2065 /// The three answers a request naming a session can get before it
2066 /// reaches anything, and the fact that separates the first two.
2067 /// A caller holding a list needs "your list was stale" and *that id was
2068 /// never mine* to be different answers, and once a session ends nothing is
2069 /// left of it but its id — so the split rests entirely on the high-water
2070 /// mark. Without it every ended session would be reported as one that never
2071 /// existed, and a control plane driven from a timeline could not tell a
2072 /// session that finished early from a typo.
2073 ///
2074 /// *Ablation, recorded:* delete the `high_water` arm from
2075 /// `ControlPlane::reach`, leaving a lookup miss to answer
2076 /// `NoSuchSession`. This test goes red with
2077 ///
2078 /// ```text
2079 /// assertion `left == right` failed: a session this proxy ran and has
2080 /// finished is a race, not a mistake
2081 /// left: NoSuchSession(SessionId(2))
2082 /// right: SessionEnded(SessionId(2))
2083 /// ```
2084 #[test]
2085 fn an_ended_session_and_an_id_that_never_ran_are_different_answers() {
2086 let plane = ControlPlane::new(LegSetup::default());
2087
2088 assert_eq!(
2089 plane.reach(SessionId(2)).err(),
2090 Some(ControlError::NoSuchSession(SessionId(2))),
2091 "nothing has ever run here"
2092 );
2093
2094 let registration = plane.register(SessionId(2), handle());
2095 assert!(plane.reach(SessionId(2)).is_ok(), "a running session is reachable");
2096
2097 drop(registration);
2098 assert_eq!(
2099 plane.reach(SessionId(2)).err(),
2100 Some(ControlError::SessionEnded(SessionId(2))),
2101 "a session this proxy ran and has finished is a race, not a mistake"
2102 );
2103 assert_eq!(
2104 plane.reach(SessionId(3)).err(),
2105 Some(ControlError::NoSuchSession(SessionId(3))),
2106 "an id above everything this proxy has minted was never its own"
2107 );
2108 }
2109
2110 /// A session whose token has fired is refused rather than acted on.
2111 ///
2112 /// Its registration is still in the map — the guard lives in the
2113 /// session's own frame and that frame is unwinding — so a lookup finds
2114 /// it. Its tasks are on their way out, though, so a request accepted
2115 /// there would be taken and never served, which is the one outcome this
2116 /// module refuses to have.
2117 #[test]
2118 fn a_session_that_is_already_going_down_takes_no_more_requests() {
2119 let plane = ControlPlane::new(LegSetup::default());
2120 let entry = handle();
2121 let cancel = entry.cancel.clone();
2122 let _registration = plane.register(SessionId(1), entry);
2123
2124 assert!(plane.reach(SessionId(1)).is_ok());
2125 cancel.cancel();
2126 assert_eq!(
2127 plane.reach(SessionId(1)).err(),
2128 Some(ControlError::SessionEnded(SessionId(1))),
2129 "still listed, no longer serving"
2130 );
2131 assert_eq!(plane.live(), vec![SessionId(1)], "and it is still listed, which is why");
2132 }
2133
2134 /// A stream key that names nothing live is refused, and the refusal
2135 /// names both the session and the key so a caller with several sessions
2136 /// can tell which lookup missed.
2137 #[test]
2138 fn resetting_a_stream_that_is_not_live_is_refused_by_key() {
2139 let plane = ControlPlane::new(LegSetup::default());
2140 let _registration = plane.register(SessionId(1), handle());
2141 let control = ProxyControl::new(Arc::clone(&plane));
2142
2143 let stream = StreamKey { side: crate::types::ProxySide::ClientToProxy, id: 4 };
2144 assert_eq!(
2145 control.reset_stream(SessionId(1), stream, 9),
2146 Err(ControlError::NoSuchStream { id: SessionId(1), stream }),
2147 "the session is live and the stream is not"
2148 );
2149 }
2150
2151 fn key(id: u64) -> StreamKey {
2152 StreamKey { side: ProxySide::ClientToProxy, id }
2153 }
2154
2155 /// The sending half of a channel whose receiver is dropped at once.
2156 ///
2157 /// These tests are about the gate and the entry's lifetime, not about
2158 /// delivery: a sender with no receiver still registers, still clones
2159 /// and still goes away with its entry, which is all the registry
2160 /// promises. A test that needed a request delivered would have to run a
2161 /// stream, and that lives in `session.rs`.
2162 fn inbox() -> tokio::sync::mpsc::Sender<StreamCommand> {
2163 tokio::sync::mpsc::channel(1).0
2164 }
2165
2166 /// A registered stream is waitable; ending it releases the gate a waiter
2167 /// already took, and the entry goes away so the *next* asker is told
2168 /// there is nothing to wait for.
2169 ///
2170 /// *Ablation:* drop the `gate.release()` from `StreamRegistry::end` —
2171 /// the `is_released` assertion goes red, which is the `max_hold` stall
2172 /// this whole mechanism exists to avoid.
2173 #[test]
2174 fn ending_a_stream_releases_the_gate_a_waiter_already_took() {
2175 let registry = Arc::new(StreamRegistry::new());
2176 let guard = registry.register(key(1), inbox());
2177
2178 let held = registry.gate_for(key(1)).expect("a live stream is waitable");
2179 assert!(!held.is_released(), "a live stream's gate is not released");
2180 assert_eq!(registry.len(), 1);
2181
2182 drop(guard);
2183
2184 assert!(held.is_released(), "ending the stream releases the gate a waiter already holds");
2185 assert!(registry.gate_for(key(1)).is_none(), "an ended stream is no longer waitable");
2186 assert_eq!(registry.len(), 0, "the entry is removed, not left released");
2187 }
2188
2189 /// A key that was never registered and a key whose stream has already
2190 /// ended are the same answer, which is what lets the caller report
2191 /// `SerializeTargetUnknown` once for both.
2192 #[test]
2193 fn an_unknown_and_an_ended_key_are_the_same_answer() {
2194 let registry = Arc::new(StreamRegistry::new());
2195 assert!(registry.gate_for(key(7)).is_none(), "never registered");
2196
2197 drop(registry.register(key(7), inbox()));
2198 assert!(registry.gate_for(key(7)).is_none(), "registered, then ended");
2199 }
2200
2201 /// Two sides may mint the same numeric id, and the registry must keep
2202 /// them apart — the runtime half of
2203 /// `a_stream_key_is_scoped_by_side_as_well_as_id`.
2204 #[test]
2205 fn the_registry_scopes_entries_by_side() {
2206 let registry = Arc::new(StreamRegistry::new());
2207 let client = StreamKey { side: ProxySide::ClientToProxy, id: 3 };
2208 let relay = StreamKey { side: ProxySide::RelayToProxy, id: 3 };
2209
2210 let _client_guard = registry.register(client, inbox());
2211 let relay_guard = registry.register(relay, inbox());
2212 assert_eq!(registry.len(), 2);
2213
2214 drop(relay_guard);
2215 assert!(registry.gate_for(client).is_some(), "the client-side stream is still live");
2216 assert!(registry.gate_for(relay).is_none());
2217 }
2218}