Skip to main content

moq_net/model/
broadcast.rs

1//! A broadcast is a named collection of tracks, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] creates tracks on demand: a [Consumer] subscribes by name, and the
4//! producer either serves a track it already has or is handed a [`track::Request`] to
5//! fill. Both handles are refcounted clones of one broadcast, which closes on
6//! [`Producer::finish`] or when the last producer drops.
7//!
8//! [Info] is the static metadata; [Route] is the dynamic path the broadcast takes to
9//! reach an origin, including whether it is announced to subscribers.
10use crate::{stats, track};
11use std::{
12	collections::{HashMap, VecDeque},
13	sync::Arc,
14	task::{Poll, ready},
15	time::Duration,
16};
17
18use crate::Error;
19
20use super::{Origin, OriginList, Requests, WeakCache};
21
22/// A collection of media tracks that can be published and subscribed to.
23///
24/// Create via [`Info::produce`] to obtain both [`Producer`] and [`Consumer`] pair.
25/// This is the broadcast's static identity, fixed for its lifetime; the path it
26/// takes to get here is the dynamic [`Route`], observed via [`Consumer::route`].
27#[derive(Clone, Debug, Default)]
28#[non_exhaustive]
29pub struct Info {
30	/// The origin this broadcast belongs to (its identity, and the cache pool its
31	/// tracks and groups inherit). A track reaches its pool by walking up this link,
32	/// so the pool has a single home on the origin rather than being copied per
33	/// broadcast. Defaults to an unknown origin with an unbounded pool (a standalone
34	/// broadcast with no relay origin).
35	pub origin: super::origin::Info,
36}
37
38impl Info {
39	/// Create a new broadcast with default metadata.
40	pub fn new() -> Self {
41		Self::default()
42	}
43
44	/// Consume this [Info] to create a producer that carries its metadata.
45	///
46	/// Keep the returned [`Producer`] alive for as long as the broadcast should stay
47	/// available, and end it with [`Producer::finish`]. See the note on [`Producer`].
48	pub fn produce(self) -> Producer {
49		Producer::new(self)
50	}
51}
52
53/// The path a broadcast takes to reach this origin, and how preferable it is.
54///
55/// Unlike [`Info`], the route is dynamic: it changes when the serving session fails
56/// over, the upstream topology shifts, or the publisher re-advertises itself.
57/// Publish a change with [`Producer::set_route`] and observe one with
58/// [`Consumer::route_changed`]; downstream sessions forward updates as a restart
59/// on the wire, so route churn never looks like a new broadcast.
60#[derive(Clone, Debug, Default, PartialEq, Eq)]
61#[non_exhaustive]
62pub struct Route {
63	/// The chain of origins the broadcast has traversed, oldest first. Each relay
64	/// appends its own [`crate::Origin`] when forwarding; used for loop detection
65	/// and as the selection tie-break.
66	pub hops: OriginList,
67
68	/// The cost of pulling the broadcast via this route, accumulated per link:
69	/// lower wins, with ties broken by hop length, then a deterministic hash, and
70	/// finally the most recently attached route.
71	///
72	/// The original publisher seeds it with its production cost (zero for a live
73	/// publish, something large for a standby that would have to start working,
74	/// like a cold transcoder), and each link adds its own configured price as
75	/// the announcement crosses it, so a route over a metered backbone ranks
76	/// worse than an equal-length one within a datacenter. The accumulation
77	/// restarts at zero at any node actively carrying the broadcast: those
78	/// upstream legs already exist and are not re-paid by one more subscriber,
79	/// so the sum is the cost of the transfers a subscription would newly cause.
80	///
81	/// Carried on the wire from lite-06; older peers always report zero, leaving
82	/// the hop-count tie-break as the effective metric exactly as before.
83	pub cost: u64,
84
85	/// The cost as the announcing peer advertised it, before this link's charge
86	/// was added to [`Self::cost`]. Local bookkeeping, never forwarded: zero on a
87	/// chain of two or more hops means the announcing relay is actively carrying
88	/// the broadcast, which is what the origin's handover gate keys on.
89	pub(crate) advertised: u64,
90
91	/// Whether the broadcast should be announced: advertised to consumers via
92	/// [`crate::origin::Consumer::announced`] while this is the best route. A
93	/// non-announced broadcast stays reachable by exact path for subscribes and
94	/// fetches (e.g. serving cached or on-demand content), so toggling this via
95	/// [`Producer::set_route`] announces or unannounces without touching the
96	/// broadcast itself. Defaults to `false`.
97	pub announce: bool,
98}
99
100impl Route {
101	/// An unannounced direct route: no hops, best cost.
102	///
103	/// The broadcast is reachable only by its exact path, so subscribers must already
104	/// know it exists. Use [`announced`](Self::announced) to advertise it instead.
105	pub fn new() -> Self {
106		Self::default()
107	}
108
109	/// An announced direct route: no hops, best cost.
110	///
111	/// The broadcast is advertised to subscribers via
112	/// [`crate::origin::Consumer::announced`] while this is the best route, on top of
113	/// staying reachable by exact path. Use [`new`](Self::new) to keep it unadvertised.
114	pub fn announced() -> Self {
115		Self {
116			announce: true,
117			..Self::default()
118		}
119	}
120
121	/// Append a hop to the chain, oldest first.
122	///
123	/// Fails with [`crate::TooManyOrigins`] once the chain is full, the same limit
124	/// the wire enforces.
125	pub fn with_hop(mut self, origin: super::Origin) -> Result<Self, super::TooManyOrigins> {
126		self.hops.push(origin)?;
127		Ok(self)
128	}
129
130	/// Replace the hop chain.
131	pub fn with_hops(mut self, hops: OriginList) -> Self {
132		self.hops = hops;
133		self
134	}
135
136	/// Set the cost: lower wins among routes serving the same broadcast.
137	pub fn with_cost(mut self, cost: u64) -> Self {
138		self.cost = cost;
139		self
140	}
141
142	/// Set whether the broadcast is announced via this route.
143	pub fn with_announce(mut self, announce: bool) -> Self {
144		self.announce = announce;
145		self
146	}
147}
148
149/// How long a drained broadcast keeps advertising a zero cost before restoring its
150/// cold one.
151///
152/// Pure hysteresis: demand edges arrive exactly (via [`Demand`]), but re-pricing the
153/// instant the last viewer leaves would flap routing across the mesh on viewer churn.
154pub(crate) const COST_LINGER: Duration = Duration::from_secs(5);
155
156/// The routes advertisable to one peer, best first: the announced ones whose hop chain
157/// avoids both the peer (`exclude`) and ourselves (a reflection), each paired with
158/// whether it is the serving route.
159///
160/// `routes` is the broadcast's table in preference order with the serving (active)
161/// route first, so a peer usually receives exactly what we serve everyone; a peer the
162/// active chain flows through receives the best standby instead of nothing. The
163/// subscribe path picks its source by the same exclusion (see
164/// [`origin::Consumer::excluding`](super::origin::Consumer::excluding)), which keeps
165/// the advertised chain truthful and the mesh loop-free.
166///
167/// Callers take the first entry they can actually stamp themselves onto, since a chain
168/// already at `MAX_HOPS` has no room and almost certainly means a loop. Empty when
169/// every chain loops through the peer or us, or none is announced.
170/// [`Origin::UNKNOWN`] identifies nothing, so it excludes nothing and is never a loop.
171pub(crate) fn advertisable_routes(
172	routes: &[Route],
173	self_origin: Origin,
174	exclude: Origin,
175) -> impl Iterator<Item = (&Route, bool)> {
176	routes.iter().enumerate().filter_map(move |(index, route)| {
177		// Offline routes are reachable by exact path but never advertised.
178		if !route.announce {
179			return None;
180		}
181		if exclude != Origin::UNKNOWN && route.hops.contains(&exclude) {
182			return None;
183		}
184		if self_origin != Origin::UNKNOWN && route.hops.contains(&self_origin) {
185			return None;
186		}
187		Some((route, index == 0))
188	})
189}
190
191/// The cost to advertise for a route.
192///
193/// While the broadcast has demand, the *serving* (active) route costs zero: our
194/// ingress is already paid for (or, for a local standby publisher, the work is already
195/// running), so one more subscriber only pays the links below us. That is what lets a
196/// cluster deduplicate onto a warm copy. A standby advertised to a peer the active
197/// chain flows through keeps its own accumulated cost, since serving that peer means
198/// opening a fresh ingest. Otherwise we forward the accumulated cost unchanged.
199///
200/// The receiving side adds its own link price on top, so this never accounts for the
201/// link we are sending over.
202pub(crate) fn outgoing_cost(demand: &Demand, route: &Route, serving: bool) -> u64 {
203	match serving && demand.is_used() {
204		true => 0,
205		false => route.cost,
206	}
207}
208
209#[derive(Default)]
210struct BroadcastState {
211	// Weak references for deduplication. Doesn't prevent track auto-close.
212	// Keyed by the track's shared `Arc<str>` name (the same Arc the handle holds).
213	// The cache reclaims closed entries incrementally on insert so a long-lived
214	// broadcast churning distinct track names stays bounded by the live count.
215	tracks: WeakCache<Arc<str>, track::TrackWeak>,
216
217	// Pending requests keyed by track name, coalescing concurrent `track()` calls
218	// and waiting for a dynamic handler to accept or deny them. A request leaves
219	// here once handed out (the handler caches it in `tracks`, so lookups keep
220	// coalescing onto it there).
221	requests: Requests<Arc<str>, track::Request>,
222
223	// Route-fed mode (a relay/origin "front"): tracks are spliced logical tracks
224	// joined across per-session tracks. `None` for an ordinary broadcast.
225	spliced: Option<SplicedState>,
226
227	// The path the broadcast currently takes to reach us, bumping `route_epoch`
228	// on every change so consumers can watch for updates.
229	route: Route,
230	route_epoch: u64,
231
232	// Every route currently attached at this path, in preference order with the
233	// serving (active) route first. Mirrored from the origin's source table for
234	// route-fed broadcasts so sessions can pick a different route per peer; an
235	// ordinary broadcast holds just its own route. `routes_epoch` bumps on any
236	// table change, including ones that leave the active route untouched (a
237	// standby attaching or repricing), which is why it is tracked separately
238	// from `route_epoch`.
239	routes: Vec<Route>,
240	routes_epoch: u64,
241
242	// Set by an explicit `Producer::finish()` or `Producer::abort()` so `Drop` can
243	// tell a deliberate shutdown apart from a producer dropped by accident.
244	closing: bool,
245
246	// Set only by `Producer::finish()`: the broadcast ended deliberately, as
247	// opposed to aborting or losing its producer. The origin reads this to decide
248	// whether a detached source may linger for a replacement.
249	finished: bool,
250
251	// The error passed to `Producer::abort()`, reported by `Consumer::closed`.
252	// `None` for a finish or a dropped producer (reported as `Error::Dropped`).
253	abort: Option<Error>,
254}
255
256/// The spliced (route-fed) half of a broadcast: logical tracks that outlive any
257/// single session, plus the queue of tracks awaiting a serving route.
258#[derive(Default)]
259struct SplicedState {
260	// Logical tracks by name, owned strongly: they live as long as the broadcast
261	// (the origin's front), not as long as any consumer.
262	tracks: HashMap<Arc<str>, super::resume::Producer>,
263
264	// Names awaiting assignment to a route, in request order.
265	pending: VecDeque<Arc<str>>,
266}
267
268impl BroadcastState {
269	/// Insert a track weak handle into the lookup, returning an error if a live
270	/// track already holds the name. A closed entry under the name is reclaimed.
271	fn insert_track(&mut self, weak: track::TrackWeak) -> Result<(), Error> {
272		match self.tracks.insert(weak.name().clone(), weak) {
273			Some(_) => Err(Error::Duplicate),
274			None => Ok(()),
275		}
276	}
277
278	/// Resolve every name the broadcast never filled, so subscribers waiting on a
279	/// [`track::Info`] that can no longer arrive fail with `err` instead of parking.
280	///
281	/// Covers a reservation nobody accepted, a request handed to a [`Dynamic`] that
282	/// never answered it, and one still queued for a handler. A track that carries
283	/// its info has a publisher and is left alone: an end there is that publisher's
284	/// call, and its cache stays readable.
285	fn reject_unserved(&mut self, err: Error) {
286		for request in self.requests.drain_queued() {
287			request.reject(err.clone());
288		}
289		for track in self.tracks.iter() {
290			track.reject(err.clone());
291		}
292	}
293
294	/// Live demand: a subscribed spliced track (route-fed broadcast), or a
295	/// pending request / consumed track (ordinary broadcast). See [`Demand`].
296	fn is_used(&self) -> bool {
297		if let Some(spliced) = &self.spliced {
298			return spliced.tracks.values().any(|track| track.is_used());
299		}
300		!self.requests.is_empty() || self.tracks.iter().any(|track| track.is_used())
301	}
302
303	/// Park `waiter` on every per-track channel feeding [`Self::is_used`]: the
304	/// consumer counts live on those channels, and their flips don't write this
305	/// state, so a watcher registered here alone would miss the edge. `want`
306	/// picks the direction; each channel only arms while its side is unmet.
307	fn register_demand(&self, waiter: &kio::Waiter, want: bool) {
308		if let Some(spliced) = &self.spliced {
309			for track in spliced.tracks.values() {
310				let _ = match want {
311					true => track.poll_used(waiter),
312					false => track.poll_unused(waiter),
313				};
314			}
315			return;
316		}
317		for track in self.tracks.iter() {
318			match want {
319				true => track.poll_used(waiter),
320				false => track.poll_unused(waiter),
321			}
322		}
323	}
324}
325
326/// Manages tracks within a broadcast.
327///
328/// Create tracks up front with [Self::create_track], reserve a name to fill in
329/// later with [Self::reserve_track], or handle on-demand consumer requests via
330/// [Self::dynamic].
331///
332/// # Lifetime
333///
334/// **You must keep this producer alive for as long as the broadcast should stay
335/// available.** A broadcast lives as long as at least one [`Producer`] exists;
336/// children do *not* keep it alive (cloning a [`Consumer`] or holding a
337/// [`track::Producer`] does nothing for the broadcast's lifetime). When the last
338/// producer goes away every consumer observes [`Error::Dropped`].
339///
340/// End the broadcast with [`Self::finish`] rather than dropping it. Dropping is an
341/// easy footgun in garbage-collected bindings (Go, Python, ...), where the handle
342/// can be collected the moment it falls out of scope even while you are still
343/// publishing, tearing the stream down mid-broadcast. Dropping the last producer
344/// without [`Self::finish`] logs a warning.
345#[derive(Clone)]
346pub struct Producer {
347	// Held behind an Arc so each track born from this broadcast can inherit a shared
348	// handle (threaded down by [`Self::create_track`] / [`Self::reserve_track`]).
349	info: Arc<Info>,
350
351	// Broadcast liveness, shared with every `Dynamic`. Consumers watch it (read-only)
352	// for close; the guard ends the broadcast when the last of those handles drops.
353	alive: Arc<Alive>,
354
355	// Track registry plus the dynamic request queue, mutated by producers and
356	// consumers alike under one lock.
357	state: kio::Shared<BroadcastState>,
358
359	// Ingress stats scope, set by a tagged `origin::Producer` at
360	// `create_broadcast`. Inherited by the tracks this producer creates. Empty
361	// (no-op) for an untagged broadcast.
362	stats: stats::Scope,
363}
364
365impl Producer {
366	/// Create a producer for the given broadcast metadata. Prefer [`Info::produce`].
367	pub fn new(info: Info) -> Self {
368		let state = kio::Shared::<BroadcastState>::default();
369		Self {
370			info: Arc::new(info),
371			alive: Alive::new(state.clone()),
372			state,
373			stats: stats::Scope::default(),
374		}
375	}
376
377	/// Attach an ingress stats scope, inherited by the tracks created on this
378	/// broadcast. Set by a tagged `origin::Producer` at `create_broadcast`.
379	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
380		self.stats = scope;
381		self
382	}
383
384	/// Create a route-fed (spliced) broadcast: consumer track lookups mint logical
385	/// tracks that are spliced across per-session tracks, queued for a route to
386	/// serve. Used by the origin for broadcasts reached over the network.
387	pub(crate) fn new_spliced(info: Info) -> Self {
388		let state = kio::Shared::new(BroadcastState {
389			spliced: Some(SplicedState::default()),
390			..Default::default()
391		});
392		Self {
393			info: Arc::new(info),
394			alive: Alive::new(state.clone()),
395			state,
396			// The origin-owned spliced broadcast stays untagged: egress attribution is
397			// applied when a tagged `origin::Consumer` hands the consumer out.
398			stats: stats::Scope::default(),
399		}
400	}
401
402	/// The broadcast's static metadata, fixed when it was created.
403	pub fn info(&self) -> &Info {
404		&self.info
405	}
406
407	/// A watch-only handle to the broadcast's demand. See [`Demand`].
408	pub fn demand(&self) -> Demand {
409		Demand {
410			alive: self.alive.token.consume().weak(),
411			state: self.state.clone(),
412		}
413	}
414
415	/// Remove a track from the lookup.
416	pub fn remove_track(&mut self, name: &str) -> Result<(), Error> {
417		self.state.lock().tracks.remove(name).ok_or(Error::NotFound)?;
418		Ok(())
419	}
420
421	/// Produce a new track and insert it into the broadcast.
422	///
423	/// Pass a name and an optional [`track::Info`], so a bare name works:
424	/// `create_track("video", None)`.
425	pub fn create_track(
426		&mut self,
427		name: impl Into<Arc<str>>,
428		info: impl Into<Option<track::Info>>,
429	) -> Result<track::Producer, Error> {
430		let name = name.into();
431		let info = info.into().unwrap_or_default();
432		let mut state = self.state.lock();
433
434		// A consumer may have requested this name before it existed (a live
435		// [`Dynamic`] queues such requests). Creating the track fulfills that
436		// request: its consumers resolve against this very producer. Without
437		// this they would be stranded, since the name is taken the moment the
438		// track exists, so no handler could ever serve their queue entry.
439		if let Some(request) = state.requests.take(name.as_ref()) {
440			let track = request.with_stats(self.stats.clone()).accept(info);
441			// Cache it like a served request so concurrent lookups coalesce; a
442			// live same-name entry cannot exist (its presence would have kept
443			// the request from queuing).
444			let _ = state.tracks.insert(name, track.weak());
445			return Ok(track);
446		}
447
448		let track = track::Producer::new(self.info.clone(), name, info).with_stats(self.stats.clone());
449		state.insert_track(track.weak())?;
450		Ok(track)
451	}
452
453	/// Reserve a track by name without finalizing its [`track::Info`].
454	///
455	/// Returns a [`track::Request`] already discoverable by consumers; call
456	/// [`track::Request::accept`] to set its info and start producing. Use this when
457	/// the producer can't pick the track's properties (e.g. timescale) until it has
458	/// inspected the media, the same shape as a consumer-driven
459	/// [`Dynamic::requested_track`].
460	///
461	/// Subscribers wait on the name until it is accepted, so a reservation the producer
462	/// ends up never filling has to be dropped or rejected. Ending the broadcast
463	/// ([`Self::finish`] or [`Self::abort`]) resolves whatever is left.
464	pub fn reserve_track(&mut self, name: impl Into<Arc<str>>) -> Result<track::Request, Error> {
465		let request = track::Request::new(self.info.clone(), name).with_stats(self.stats.clone());
466		self.state.lock().insert_track(request.weak())?;
467		Ok(request)
468	}
469
470	/// Create a track with a unique name using the given suffix.
471	///
472	/// Generates names like `0{suffix}`, `1{suffix}`, etc. and picks the first
473	/// one not already used in this broadcast.
474	pub fn unique_track(
475		&mut self,
476		suffix: &str,
477		info: impl Into<Option<track::Info>>,
478	) -> Result<track::Producer, Error> {
479		let name = self.unique_name(suffix);
480		self.create_track(name, info)
481	}
482
483	/// Generate a unique track name from a suffix without creating the track.
484	///
485	/// Returns a fresh name like `0{suffix}`, `1{suffix}`, etc. Use this when
486	/// you need to set non-default Track properties (e.g. `with_timescale`,
487	/// `with_latency_max`) before handing the Track to [`Self::create_track`].
488	pub fn unique_name(&self, suffix: &str) -> String {
489		let state = self.state.read();
490		(0u16..)
491			.map(|i| format!("{i}{suffix}"))
492			.find(|name| !state.tracks.contains_key(name.as_str()))
493			.expect("u16 namespace exhausted; wow")
494	}
495
496	/// Create a dynamic producer that handles on-demand track requests from consumers.
497	pub fn dynamic(&self) -> Dynamic {
498		Dynamic::new(
499			self.info.clone(),
500			self.alive.clone(),
501			self.state.clone(),
502			self.stats.clone(),
503		)
504	}
505
506	/// Set the broadcast's [`Route`]: the hop chain and cost it advertises.
507	///
508	/// Call this when the path to the content changes (an upstream failover) or the
509	/// publisher's preference changes (e.g. a transcoder warming up lowers its
510	/// cost). Consumers observe the change via [`Consumer::route_changed`] and
511	/// sessions forward it downstream as a restart, never as a new broadcast.
512	/// Setting the current route again is a no-op.
513	pub fn set_route(&mut self, route: Route) -> Result<(), Error> {
514		let mut state = self.state.lock();
515		if state.route == route {
516			return Ok(());
517		}
518		state.route = route.clone();
519		state.route_epoch += 1;
520		// An ordinary broadcast's table is just its own route; a route-fed one is
521		// overwritten by the next `set_routes` from the origin.
522		state.routes = vec![route];
523		state.routes_epoch += 1;
524		Ok(())
525	}
526
527	/// Replace the full route table, in preference order with the active route
528	/// first. Set by the origin's front on every source-table change; the active
529	/// route doubles as the broadcast's advertised [`Route`].
530	///
531	/// `routes` must be non-empty. A front whose table empties is on its way out,
532	/// and it unannounces and aborts rather than advertising a "no route" route,
533	/// so there is no such value to publish here.
534	pub(crate) fn set_routes(&mut self, routes: Vec<Route>) {
535		debug_assert!(!routes.is_empty(), "set_routes requires a non-empty table");
536		let mut state = self.state.lock();
537		if let Some(active) = routes.first()
538			&& state.route != *active
539		{
540			state.route = active.clone();
541			state.route_epoch += 1;
542		}
543		if state.routes != routes {
544			state.routes = routes;
545			state.routes_epoch += 1;
546		}
547	}
548
549	/// Poll for the next spliced track awaiting a serving route, returning its name
550	/// and logical producer. Route-fed broadcasts only.
551	pub(crate) fn poll_spliced_assigned(&self, waiter: &kio::Waiter) -> Poll<(Arc<str>, super::resume::Producer)> {
552		let mut state = ready!(self.state.poll(waiter, |state| {
553			match &state.spliced {
554				Some(spliced) if !spliced.pending.is_empty() => Poll::Ready(()),
555				_ => Poll::Pending,
556			}
557		}));
558
559		let spliced = state.spliced.as_mut().expect("predicate guaranteed spliced");
560		let name = spliced.pending.pop_front().expect("predicate guaranteed a request");
561		let producer = spliced.tracks.get(&name).expect("pending name without a track").clone();
562		Poll::Ready((name, producer))
563	}
564
565	/// Abort every spliced track, releasing their subscribers with `err`. Called
566	/// when the broadcast closes for good.
567	pub(crate) fn abort_spliced(&self, err: Error) {
568		let mut state = self.state.lock();
569		if let Some(spliced) = state.spliced.as_mut() {
570			spliced.pending.clear();
571			for producer in spliced.tracks.values_mut() {
572				let _ = producer.abort(err.clone());
573			}
574		}
575	}
576
577	/// Create a consumer that can subscribe to tracks in this broadcast.
578	pub fn consume(&self) -> Consumer {
579		Consumer {
580			info: self.info.clone(),
581			alive: self.alive.token.consume(),
582			state: self.state.clone(),
583			route_seen: None,
584			routes_seen: None,
585			stats: stats::Scope::default(),
586			exclusion: None,
587		}
588	}
589
590	/// Cleanly finish the broadcast once you are done publishing.
591	///
592	/// Marks the broadcast as deliberately finished so consumers observe a normal
593	/// end. Prefer this over dropping the producer: an accidental drop (see the note
594	/// on [`Producer`]) logs a warning, whereas `finish()` is silent.
595	///
596	/// Ends the broadcast outright: consumers observe a normal end immediately and no
597	/// new tracks are served, whether or not other producer clones are still alive.
598	/// Existing tracks stay readable so consumers can drain what they already have.
599	///
600	/// A name that was reserved or requested but never served resolves with
601	/// [`Error::NotFound`]: nothing can fill it now, so its subscribers fail rather
602	/// than waiting on a [`track::Info`] that is never coming.
603	///
604	/// Borrows rather than consumes, matching [`track::Producer::finish`]. Finishing
605	/// declares the end, so it must not depend on the caller also surrendering the
606	/// handle.
607	pub fn finish(&mut self) {
608		{
609			let mut state = self.state.lock();
610			state.closing = true;
611			state.finished = true;
612			// A name that was reserved or requested but never served can't arrive now,
613			// and `Consumer::track` already answers `NotFound` for one asked about after
614			// this point. Say the same to whoever asked earlier.
615			state.reject_unserved(Error::NotFound);
616		}
617		// Ending the broadcast is what consumers wait on, so signal it here rather
618		// than leaving it to the last handle drop.
619		let _ = self.alive.token.close();
620	}
621
622	/// Abort the broadcast, ending it for consumers with `err`.
623	///
624	/// Like [`finish`](Self::finish) the end is immediate, whether or not other
625	/// producer clones are still alive, and existing tracks stay readable so
626	/// consumers can drain what they already have (an abort does not cascade into
627	/// the tracks), while a name nothing ever served resolves with `err` the same
628	/// way [`finish`](Self::finish) resolves it. Unlike a finish, consumers observe `err` from
629	/// [`Consumer::closed`], and an origin treats the source as ungracefully lost,
630	/// so the path may linger for a replacement (see
631	/// [`origin::Info::linger`](crate::origin::Info::linger)).
632	///
633	/// Consumes the producer: an abort is terminal. Errors if the broadcast was
634	/// already finished or aborted.
635	pub fn abort(self, err: Error) -> Result<(), Error> {
636		{
637			let mut state = self.state.lock();
638			if state.closing {
639				return Err(Error::Closed);
640			}
641			state.closing = true;
642			state.abort = Some(err.clone());
643			// Same as a finish: an unserved name is answerable now, with the reason the
644			// broadcast ended. Published tracks keep their cache (no cascade).
645			state.reject_unserved(err);
646		}
647		let _ = self.alive.token.close();
648		Ok(())
649	}
650
651	/// Return true if this is the same broadcast instance.
652	pub fn is_clone(&self, other: &Self) -> bool {
653		self.state.same_channel(&other.state)
654	}
655}
656
657/// Ends the broadcast when the last [`Producer`] or [`Dynamic`] drops, closing the
658/// liveness channel every [`Consumer`] watches.
659///
660/// A refcount rather than a "am I the last one?" check inside `Drop`: that answer is
661/// a snapshot, and acting on it is exactly what invalidates it.
662struct Alive {
663	token: kio::Producer<()>,
664	state: kio::Shared<BroadcastState>,
665}
666
667impl Alive {
668	fn new(state: kio::Shared<BroadcastState>) -> Arc<Self> {
669		Arc::new(Self {
670			token: kio::Producer::default(),
671			state,
672		})
673	}
674}
675
676impl Drop for Alive {
677	fn drop(&mut self) {
678		// Warn if the last exit wasn't an explicit finish(), since consumers will
679		// then see Error::Dropped (classically a GC-collected handle in a language
680		// binding that tears the stream down mid-publish).
681		if !self.state.read().closing {
682			tracing::warn!(
683				"broadcast::Producer dropped without finish(). Keep the producer alive while publishing, then call finish()."
684			);
685		}
686	}
687}
688
689#[cfg(test)]
690#[allow(missing_docs)] // test-only assertion helpers
691impl Producer {
692	pub fn assert_create_track(
693		&mut self,
694		name: impl Into<Arc<str>>,
695		info: impl Into<Option<track::Info>>,
696	) -> track::Producer {
697		self.create_track(name, info).expect("should not have errored")
698	}
699}
700
701/// A session-owned handle to a source broadcast created via
702/// [`crate::origin::Producer::create_broadcast`]: [`Self::finish`] ends it
703/// deliberately, while dropping the guard aborts it as [`Error::Dropped`] (a dead
704/// session), letting the origin linger the path for a reconnect. Shared by the
705/// lite and IETF subscribers so the drop-vs-finish contract lives in one place.
706pub(crate) struct SourceGuard {
707	// `Option` so `finish` can consume the producer while `Drop` aborts it.
708	producer: Option<Producer>,
709}
710
711impl SourceGuard {
712	pub fn new(producer: Producer) -> Self {
713		Self {
714			producer: Some(producer),
715		}
716	}
717
718	/// A clone of the guarded producer.
719	pub fn producer(&self) -> Producer {
720		self.producer.clone().expect("guard holds a producer until finished")
721	}
722
723	/// End the source deliberately: the origin detaches it immediately,
724	/// unannouncing the path if it was the last.
725	pub fn finish(mut self) {
726		if let Some(mut producer) = self.producer.take() {
727			producer.finish();
728		}
729	}
730
731	/// Update the source's advertised route in place.
732	pub fn set_route(&mut self, route: Route) {
733		if let Some(producer) = &mut self.producer {
734			let _ = producer.set_route(route);
735		}
736	}
737}
738
739impl Drop for SourceGuard {
740	fn drop(&mut self) {
741		if let Some(producer) = self.producer.take() {
742			let _ = producer.abort(Error::Dropped);
743		}
744	}
745}
746
747/// Handles on-demand track creation for a broadcast.
748///
749/// When a consumer requests a track that doesn't exist, the dynamic producer
750/// picks up the request via [`Self::requested_track`] and either
751/// [`track::Request::accept`]s it with a concrete [`track::Info`] or
752/// [`track::Request::reject`]s it. Dropped when no longer needed; pending requests
753/// are automatically aborted.
754pub struct Dynamic {
755	info: Arc<Info>,
756	// Keeps the broadcast alive while a handler exists (mirrors a producer).
757	alive: Arc<Alive>,
758	state: kio::Shared<BroadcastState>,
759	// Ingress stats scope, applied to the tracks this handler serves. Empty (no-op)
760	// for an untagged broadcast.
761	stats: stats::Scope,
762}
763
764impl Clone for Dynamic {
765	fn clone(&self) -> Self {
766		// Mirror `new`: count each live handle. Without this, deriving Clone would
767		// let `Drop` decrement past `new`'s single increment and prematurely flip
768		// the handler count to zero, causing future `track` calls to return `NotFound`.
769		self.state.lock().requests.add_handler();
770
771		Self {
772			info: self.info.clone(),
773			alive: self.alive.clone(),
774			state: self.state.clone(),
775			stats: self.stats.clone(),
776		}
777	}
778}
779
780impl Dynamic {
781	fn new(info: Arc<Info>, alive: Arc<Alive>, state: kio::Shared<BroadcastState>, stats: stats::Scope) -> Self {
782		state.lock().requests.add_handler();
783
784		Self {
785			info,
786			alive,
787			state,
788			stats,
789		}
790	}
791
792	/// The broadcast's static metadata, fixed when it was created.
793	pub fn info(&self) -> &Info {
794		&self.info
795	}
796
797	/// Poll for the next consumer-requested track, without blocking.
798	///
799	/// Returns [`Error::Closed`] once the broadcast was deliberately ended
800	/// ([`Producer::finish`] or aborted), so a serving loop knows to stop and
801	/// release its handle.
802	pub fn poll_requested_track(&mut self, waiter: &kio::Waiter) -> Poll<Result<track::Request, Error>> {
803		let mut state = ready!(self.state.poll(waiter, |state| {
804			if state.requests.has_queued() || state.closing {
805				Poll::Ready(())
806			} else {
807				Poll::Pending
808			}
809		}));
810
811		if state.closing && !state.requests.has_queued() {
812			return Poll::Ready(Err(Error::Closed));
813		}
814
815		let name = state.requests.pop().expect("predicate guaranteed a request");
816		let pending = state.requests.remove(&name).expect("popped key must be pending");
817		// Cache the served track so concurrent lookups coalesce onto it. If a live track already
818		// holds the name (a publish raced the request), `insert` keeps it rather than shadowing it.
819		let _ = state.tracks.insert(name, pending.weak());
820		// Attribute the served track to this broadcast's ingress scope (no-op untagged).
821		Poll::Ready(Ok(pending.with_stats(self.stats.clone())))
822	}
823
824	/// Block until a consumer requests a track, returning a [`track::Request`] to serve.
825	pub async fn requested_track(&mut self) -> Result<track::Request, Error> {
826		kio::wait(|waiter| self.poll_requested_track(waiter)).await
827	}
828
829	/// Create a consumer that can subscribe to tracks in this broadcast.
830	pub fn consume(&self) -> Consumer {
831		Consumer {
832			info: self.info.clone(),
833			alive: self.alive.token.consume(),
834			state: self.state.clone(),
835			route_seen: None,
836			routes_seen: None,
837			stats: stats::Scope::default(),
838			exclusion: None,
839		}
840	}
841
842	/// Block until the broadcast is closed, by [`Producer::finish`],
843	/// [`Producer::abort`], or every producer dropping, returning the cause.
844	pub async fn closed(&self) -> Error {
845		kio::wait(|waiter| self.poll_closed(waiter)).await
846	}
847
848	/// Poll until the broadcast closes; ready with the cause: the error passed to
849	/// [`Producer::abort`], or [`Error::Dropped`] for a [`Producer::finish`] or a
850	/// dropped producer (check [`Consumer::is_finished`] to tell those apart).
851	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
852		ready!(self.alive.token.poll_closed(waiter));
853		Poll::Ready(self.state.read().abort.clone().unwrap_or(Error::Dropped))
854	}
855
856	/// Return true if this is the same broadcast instance.
857	pub fn is_clone(&self, other: &Self) -> bool {
858		self.state.same_channel(&other.state)
859	}
860}
861
862impl Drop for Dynamic {
863	fn drop(&mut self) {
864		// Decrement and reject under one lock, so a `track` call that saw a live
865		// handler through the same lock can't slip a request past the rejection.
866		let mut state = self.state.lock();
867		if state.requests.remove_handler() {
868			// No handlers left to fulfill pending requests; reject them so consumers
869			// don't block forever on tracks nobody will serve.
870			for request in state.requests.drain_queued() {
871				request.reject(Error::Dropped);
872			}
873		}
874	}
875}
876
877#[cfg(test)]
878use futures::FutureExt;
879
880#[cfg(test)]
881#[allow(missing_docs)] // test-only assertion helpers
882impl Dynamic {
883	pub fn assert_request(&mut self) -> track::Request {
884		self.requested_track()
885			.now_or_never()
886			.expect("should not have blocked")
887			.expect("should not have errored")
888	}
889
890	pub fn assert_no_request(&mut self) {
891		assert!(self.requested_track().now_or_never().is_none(), "should have blocked");
892	}
893}
894
895/// Subscribe to arbitrary broadcast/tracks.
896pub struct Consumer {
897	info: Arc<Info>,
898	// Broadcast liveness (read-only): watched for close.
899	alive: kio::Consumer<()>,
900	// Track registry plus request queue; `track()` reads the registry and enqueues requests.
901	state: kio::Shared<BroadcastState>,
902	// The route epoch last yielded by `route_changed`, so each consumer clone
903	// observes the current route first and every change after it exactly once.
904	route_seen: Option<u64>,
905	// Same cursor for the full route table (`routes_changed`), tracked separately
906	// because the table can change without the active route moving.
907	routes_seen: Option<u64>,
908	// Egress stats scope, set by a tagged `origin::Consumer` at the broadcast
909	// handoff. Inherited by the tracks subscribed through this handle. Empty (no-op)
910	// for an untagged broadcast.
911	stats: stats::Scope,
912	// Keeps the origin's front off routes that flow back through the peer this
913	// handle was resolved for, released when the last clone drops. Only set on the
914	// shared front of a route-fed broadcast, and only for a peer that declared an
915	// origin; `None` everywhere else.
916	exclusion: Option<Arc<super::origin_impl::ExclusionGuard>>,
917}
918
919impl Clone for Consumer {
920	fn clone(&self) -> Self {
921		Self {
922			info: self.info.clone(),
923			alive: self.alive.clone(),
924			state: self.state.clone(),
925			// Reset the cursor so the clone observes the current route first,
926			// even if the original already drained `route_changed`.
927			route_seen: None,
928			routes_seen: None,
929			stats: self.stats.clone(),
930			exclusion: self.exclusion.clone(),
931		}
932	}
933}
934
935impl Consumer {
936	/// Attach the guard that keeps the origin's front off routes flowing back
937	/// through the peer this handle was resolved for. Set once, at the origin's
938	/// broadcast handoff; the guard is shared by every clone of this handle.
939	pub(crate) fn with_exclusion(mut self, guard: Arc<super::origin_impl::ExclusionGuard>) -> Self {
940		self.exclusion = Some(guard);
941		self
942	}
943
944	/// Attach an egress stats scope, inherited by the tracks subscribed through this
945	/// handle. Set by a tagged `origin::Consumer` at the broadcast handoff.
946	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
947		self.stats = scope;
948		self
949	}
950
951	/// The broadcast's static metadata, fixed when it was created.
952	pub fn info(&self) -> &Info {
953		&self.info
954	}
955
956	/// The [`Route`] the broadcast currently takes to reach this origin.
957	pub fn route(&self) -> Route {
958		self.state.read().route.clone()
959	}
960
961	/// Poll for a route change. See [`Self::route_changed`].
962	pub fn poll_route_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Route, Error>> {
963		let seen = self.route_seen;
964		if let Poll::Ready(state) = self.state.poll(waiter, |state| {
965			if seen != Some(state.route_epoch) {
966				Poll::Ready(())
967			} else {
968				Poll::Pending
969			}
970		}) {
971			self.route_seen = Some(state.route_epoch);
972			return Poll::Ready(Ok(state.route.clone()));
973		}
974		// No pending change: surface the broadcast's end instead of parking forever.
975		ready!(self.alive.poll_closed(waiter));
976		Poll::Ready(Err(Error::Dropped))
977	}
978
979	/// Wait for the broadcast's [`Route`] to change.
980	///
981	/// The first call returns the current route immediately; each later call blocks
982	/// until it changes again, so a loop observes the initial value followed by
983	/// every update. Returns [`Error::Dropped`] once every producer is gone.
984	pub async fn route_changed(&mut self) -> Result<Route, Error> {
985		kio::wait(|waiter| self.poll_route_changed(waiter)).await
986	}
987
988	/// Every route currently attached at this path, in preference order with the
989	/// serving (active) route first. An ordinary broadcast holds just its own
990	/// route; a route-fed one mirrors the origin's source table so sessions can
991	/// advertise a different route per peer.
992	pub(crate) fn routes(&self) -> Vec<Route> {
993		self.state.read().routes.clone()
994	}
995
996	/// Poll for any change to the route table, including ones that leave the
997	/// active route untouched (a standby attaching, detaching, or repricing).
998	/// The first call is ready immediately; read the table with [`Self::routes`].
999	pub(crate) fn poll_routes_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1000		let seen = self.routes_seen;
1001		if let Poll::Ready(state) = self.state.poll(waiter, |state| {
1002			if seen != Some(state.routes_epoch) {
1003				Poll::Ready(())
1004			} else {
1005				Poll::Pending
1006			}
1007		}) {
1008			self.routes_seen = Some(state.routes_epoch);
1009			return Poll::Ready(Ok(()));
1010		}
1011		// No pending change: surface the broadcast's end instead of parking forever.
1012		ready!(self.alive.poll_closed(waiter));
1013		Poll::Ready(Err(Error::Dropped))
1014	}
1015
1016	/// Get a handle to a track on this broadcast.
1017	pub fn track(&self, name: &str) -> Result<track::Consumer, Error> {
1018		// Tag the resolved track with this broadcast's egress scope so its
1019		// subscriptions, fetches, and groups are attributed to the same broadcast.
1020		self.track_inner(name).map(|track| track.with_stats(self.stats.clone()))
1021	}
1022
1023	fn track_inner(&self, name: &str) -> Result<track::Consumer, Error> {
1024		// A closed broadcast (every producer and handler gone) serves nothing.
1025		if self.is_closed() {
1026			return Err(Error::Dropped);
1027		}
1028
1029		let mut state = self.state.lock();
1030
1031		// A route-fed broadcast mints spliced logical tracks: they outlive any
1032		// session, and a route is asked (via the pending queue) to start serving.
1033		let closing = state.closing;
1034		if let Some(spliced) = state.spliced.as_mut() {
1035			// An aborted logical track is a verdict from the sources attached at
1036			// the time, not a property of the name: a publisher that had not yet
1037			// created the track may have it now. Drop it so this request reaches a
1038			// source again, exactly as the plain lookup below reclaims a closed
1039			// entry. A *finished* one stays, since its cache is still readable.
1040			//
1041			// So a name, once finished, never comes back here: a publisher that
1042			// finishes a track and publishes it again is serving new content, not
1043			// resuming this one, and a subscriber has to re-read the catalog and
1044			// re-initialize rather than be spliced onto it. Resuming the same
1045			// content across routes is the transparent case, and that is what
1046			// `resume::Producer` already does. Publish new content under a new
1047			// name.
1048			if spliced.tracks.get(name).is_some_and(|track| track.is_aborted()) {
1049				spliced.tracks.remove(name);
1050			}
1051			if let Some(producer) = spliced.tracks.get(name) {
1052				return Ok(track::Consumer::spliced(name.into(), producer.consume()));
1053			}
1054			// A deliberately-ended broadcast serves nothing new; nothing drains the
1055			// pending queue once the front is torn down.
1056			if closing {
1057				return Err(Error::NotFound);
1058			}
1059			let name: Arc<str> = name.into();
1060			let producer = super::resume::Producer::new();
1061			let consumer = producer.consume();
1062			spliced.tracks.insert(name.clone(), producer);
1063			spliced.pending.push_back(name.clone());
1064			return Ok(track::Consumer::spliced(name, consumer));
1065		}
1066
1067		// Reuse a live producer if one is already publishing the track. `get` drops a
1068		// closed entry and returns `None`, so we fall through to a fresh request.
1069		if let Some(weak) = state.tracks.get(name) {
1070			return Ok(weak.consume());
1071		}
1072
1073		if let Some(pending) = state.requests.join(name) {
1074			// Coalesce onto a queued request for the same name.
1075			return Ok(pending.consume());
1076		}
1077
1078		// A deliberately-ended broadcast serves nothing new; existing tracks above
1079		// stay readable so consumers can drain the cache.
1080		if state.closing {
1081			return Err(Error::NotFound);
1082		}
1083
1084		// Allocate the name once and share the same Arc across the request, the
1085		// requests map, and the FIFO order. The request inherits the broadcast's
1086		// cache pool through its `Arc<Info>`, same as a producer-created track.
1087		let name: Arc<str> = name.into();
1088		let request = track::Request::new(self.info.clone(), name.clone());
1089		let consumer = request.consume();
1090
1091		// With no handler alive to serve it, the request is dropped: `NotFound` beats
1092		// handing back a consumer that would only resolve `Dropped`.
1093		if state.requests.insert(name, request).is_err() {
1094			return Err(Error::NotFound);
1095		}
1096
1097		Ok(consumer)
1098	}
1099
1100	/// A watch-only handle to the broadcast's demand. See [`Demand`].
1101	///
1102	/// The consumer-side sibling of [`Producer::demand`], for a holder that has
1103	/// only a read handle: a relay pulling a broadcast from upstream owns no
1104	/// producer for it (the ingesting session does), yet the question it has to
1105	/// answer is whether anything downstream is still reading. Holding this
1106	/// handle, or the [`Consumer`] it came from, is not itself demand.
1107	///
1108	/// Two endings a caller has to tell apart. Demand going away is
1109	/// [`Demand::unused`] resolving, and means nobody downstream is reading.
1110	/// The broadcast going away is [`Error::Dropped`], and here that is the
1111	/// upstream producer, not the readers.
1112	pub fn demand(&self) -> Demand {
1113		Demand {
1114			alive: self.alive.weak(),
1115			state: self.state.clone(),
1116		}
1117	}
1118
1119	/// Block until the broadcast is closed, by [`Producer::finish`],
1120	/// [`Producer::abort`], or every producer dropping, and return the cause.
1121	///
1122	/// Returns the error passed to [`Producer::abort`], or [`Error::Dropped`] for a
1123	/// [`Producer::finish`] or a dropped producer (check [`Self::is_finished`] to
1124	/// tell those apart).
1125	pub async fn closed(&self) -> Error {
1126		self.alive.closed().await;
1127		self.state.read().abort.clone().unwrap_or(Error::Dropped)
1128	}
1129
1130	/// Returns true if every [`Producer`] has been dropped.
1131	pub fn is_closed(&self) -> bool {
1132		self.alive.is_closed()
1133	}
1134
1135	/// Whether the broadcast is on its way out: deliberately ended (finish/abort
1136	/// marked, even while handles remain) or already fully closed. The origin's
1137	/// dispatcher treats a rejection from such a source as imminent detach rather
1138	/// than a strike.
1139	pub(crate) fn is_closing(&self) -> bool {
1140		self.is_closed() || self.state.read().closing
1141	}
1142
1143	/// Whether the broadcast ended via a deliberate [`Producer::finish`], as opposed
1144	/// to aborting or losing its producer. `false` while the broadcast is still live;
1145	/// an origin uses this to close a front immediately on a deliberate end instead
1146	/// of lingering for a replacement.
1147	pub fn is_finished(&self) -> bool {
1148		self.state.read().finished
1149	}
1150
1151	/// Register a [`kio::Waiter`] that fires when the broadcast closes.
1152	///
1153	/// Returns [`Poll::Ready`] if already closed, otherwise [`Poll::Pending`] after
1154	/// arming the waiter. Useful for composing close-detection into a larger poll
1155	/// without spawning a task per broadcast.
1156	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
1157		self.alive.poll_closed(waiter)
1158	}
1159
1160	/// Check if this is the exact same instance of a broadcast.
1161	pub fn is_clone(&self, other: &Self) -> bool {
1162		self.state.same_channel(&other.state)
1163	}
1164
1165	/// Create a weak reference that doesn't keep the broadcast alive.
1166	///
1167	/// Used to deduplicate dynamically-served broadcasts in the origin: a live weak yields
1168	/// a shared clone, a closed one is discarded so the next request re-serves.
1169	pub(crate) fn weak(&self) -> WeakConsumer {
1170		WeakConsumer {
1171			info: self.info.clone(),
1172			alive: self.alive.weak(),
1173			state: self.state.clone(),
1174		}
1175	}
1176}
1177
1178/// A weak reference to a broadcast that doesn't prevent it from closing.
1179///
1180/// Mirrors [`track::TrackWeak`]: held by the origin's dynamic cache to share one
1181/// dynamically-served broadcast across repeat requests without pinning it alive.
1182/// Only the `alive` handle needs to be weak; a [`kio::Shared`] carries no liveness,
1183/// so holding the state outright pins nothing.
1184#[derive(Clone)]
1185pub(crate) struct WeakConsumer {
1186	info: Arc<Info>,
1187	alive: kio::ConsumerWeak<()>,
1188	state: kio::Shared<BroadcastState>,
1189}
1190
1191impl WeakConsumer {
1192	/// Upgrade to a full [`Consumer`] sharing the same broadcast state.
1193	pub fn consume(&self) -> Consumer {
1194		Consumer {
1195			info: self.info.clone(),
1196			alive: self.alive.consume(),
1197			state: self.state.clone(),
1198			route_seen: None,
1199			routes_seen: None,
1200			stats: stats::Scope::default(),
1201			exclusion: None,
1202		}
1203	}
1204}
1205
1206impl super::WeakEntry for WeakConsumer {
1207	fn is_closed(&self) -> bool {
1208		self.alive.is_closed()
1209	}
1210
1211	fn same_channel(&self, other: &Self) -> bool {
1212		self.state.same_channel(&other.state)
1213	}
1214}
1215
1216/// A cloneable, watch-only handle to a broadcast's subscriber demand.
1217///
1218/// Obtained from [`Producer::demand`] or [`Consumer::demand`]; the broadcast-level
1219/// sibling of [`track::Demand`](crate::track::Demand). Demand means live interest in the
1220/// broadcast's content: a subscribed spliced track on a route-fed broadcast, or
1221/// a pending track request / a consumed track on an ordinary one. A publisher
1222/// uses it to run expensive work only while someone is watching, and routing
1223/// uses it to advertise a warm copy at zero cost.
1224///
1225/// It's a weak handle: it neither keeps the broadcast alive nor counts as
1226/// demand itself. Once every producer is gone, [`used`](Self::used) /
1227/// [`unused`](Self::unused) return [`Error::Dropped`].
1228#[derive(Clone)]
1229pub struct Demand {
1230	alive: kio::ConsumerWeak<()>,
1231	state: kio::Shared<BroadcastState>,
1232}
1233
1234impl Demand {
1235	/// Whether the broadcast has live demand right now.
1236	///
1237	/// A point-in-time snapshot with no registration; use [`Self::used`] /
1238	/// [`Self::unused`] (or their `poll_*` forms) to wait for the edge.
1239	pub fn is_used(&self) -> bool {
1240		self.state.read().is_used()
1241	}
1242
1243	/// Block until the broadcast has demand. Resolves immediately if it already
1244	/// does; returns [`Error::Dropped`] once every producer is gone.
1245	pub async fn used(&self) -> Result<(), Error> {
1246		kio::wait(|waiter| self.poll_used(waiter)).await
1247	}
1248
1249	/// Block until the broadcast has no demand. Resolves immediately if it has
1250	/// none; returns [`Error::Dropped`] once every producer is gone.
1251	pub async fn unused(&self) -> Result<(), Error> {
1252		kio::wait(|waiter| self.poll_unused(waiter)).await
1253	}
1254
1255	/// Poll-based variant of [`Self::used`].
1256	pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1257		self.poll_demand(waiter, true)
1258	}
1259
1260	/// Poll-based variant of [`Self::unused`].
1261	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1262		self.poll_demand(waiter, false)
1263	}
1264
1265	fn poll_demand(&self, waiter: &kio::Waiter, want: bool) -> Poll<Result<(), Error>> {
1266		// Closure is checked first, matching `track::Demand`: a dead broadcast
1267		// reports Dropped rather than pretending to answer.
1268		if self.alive.poll_closed(waiter).is_ready() {
1269			return Poll::Ready(Err(Error::Dropped));
1270		}
1271		let ready = self.state.poll(waiter, |state| {
1272			// The consumer counts live on the per-track channels, whose flips
1273			// don't write this state: park on those channels too so the edge
1274			// wakes us, then recompute here.
1275			state.register_demand(waiter, want);
1276			match state.is_used() == want {
1277				true => Poll::Ready(()),
1278				false => Poll::Pending,
1279			}
1280		});
1281		match ready {
1282			Poll::Ready(_) => Poll::Ready(Ok(())),
1283			Poll::Pending => Poll::Pending,
1284		}
1285	}
1286}
1287
1288#[cfg(test)]
1289#[allow(missing_docs)] // test-only assertion helpers
1290impl Consumer {
1291	pub fn assert_not_closed(&self) {
1292		assert!(self.closed().now_or_never().is_none(), "should not be closed");
1293	}
1294
1295	pub fn assert_closed(&self) {
1296		assert!(self.closed().now_or_never().is_some(), "should be closed");
1297	}
1298}
1299
1300#[cfg(test)]
1301mod test {
1302	use super::*;
1303
1304	/// Await with a timeout so a missed demand wake fails the test instead of
1305	/// hanging it (time is paused, so the timeout fires instantly when idle).
1306	async fn expect<T>(fut: impl Future<Output = T>) -> T {
1307		tokio::time::timeout(std::time::Duration::from_secs(1), fut)
1308			.await
1309			.expect("timed out waiting for a demand edge")
1310	}
1311
1312	/// Demand on an ordinary broadcast tracks subscriber interest, not
1313	/// production: a live track producer alone is unused, a consumed track is
1314	/// used, and both edges wake parked waiters.
1315	#[tokio::test]
1316	async fn demand_ordinary() {
1317		tokio::time::pause();
1318
1319		let mut producer = Info::new().produce();
1320		let consumer = producer.consume();
1321		let demand = producer.demand();
1322
1323		// No demand yet; `unused` resolves immediately.
1324		assert!(!demand.is_used());
1325		demand.unused().await.unwrap();
1326
1327		// Producing alone is not demand.
1328		let _track = producer.create_track("a", None).unwrap();
1329		assert!(!demand.is_used());
1330
1331		// A consumer appearing wakes a parked `used`.
1332		let (used, handle) = tokio::join!(expect(demand.used()), async { consumer.track("a").unwrap() });
1333		used.unwrap();
1334		assert!(demand.is_used());
1335
1336		// The last consumer dropping wakes a parked `unused`.
1337		let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(handle) });
1338		unused.unwrap();
1339		assert!(!demand.is_used());
1340
1341		// Every producer gone: both edges report the closure.
1342		producer.finish();
1343		assert!(matches!(demand.used().await, Err(Error::Dropped)));
1344		assert!(matches!(demand.unused().await, Err(Error::Dropped)));
1345	}
1346
1347	/// Demand on a spliced (route-fed) broadcast follows the logical tracks'
1348	/// consumers, which is what flips a relay's advertised cost.
1349	#[tokio::test]
1350	async fn demand_spliced() {
1351		tokio::time::pause();
1352
1353		let producer = Producer::new_spliced(Info::new());
1354		let consumer = producer.consume();
1355		let demand = producer.demand();
1356		// The read handle answers the same question, which is all a relay
1357		// holding a pulled broadcast has.
1358		let watched = consumer.demand();
1359
1360		assert!(!demand.is_used());
1361		assert!(!watched.is_used());
1362		let track = consumer.track("video").unwrap();
1363		assert!(demand.is_used());
1364		assert!(watched.is_used());
1365
1366		// Dropping the only consumer wakes a parked `unused`, even though the
1367		// logical track itself stays cached in the broadcast. Parked on the read
1368		// handle: that edge is what tells a relay its pull has no readers left.
1369		let (unused, ()) = tokio::join!(expect(watched.unused()), async { drop(track) });
1370		unused.unwrap();
1371		assert!(!demand.is_used());
1372		assert!(!watched.is_used());
1373
1374		// A repeat consumer for the cached track counts again.
1375		let _track = consumer.track("video").unwrap();
1376		assert!(demand.is_used());
1377	}
1378
1379	/// A read handle's demand reports the producer going away as `Dropped`,
1380	/// distinct from its readers going away.
1381	///
1382	/// The distinction a relay has to act on: no readers means stop pulling,
1383	/// while an upstream that vanished means the pull is over. Both arrive on
1384	/// the same handle, so the two have to be told apart by their result rather
1385	/// than by which one resolved.
1386	#[tokio::test]
1387	async fn a_read_handle_reports_a_dropped_producer_apart_from_lost_demand() {
1388		let producer = Producer::new_spliced(Info::new());
1389		let consumer = producer.consume();
1390		let watched = consumer.demand();
1391
1392		let track = consumer.track("video").unwrap();
1393		assert!(watched.is_used());
1394
1395		// Readers go, the broadcast stays: demand ends, and the handle keeps
1396		// answering.
1397		let (unused, ()) = tokio::join!(expect(watched.unused()), async { drop(track) });
1398		unused.unwrap();
1399
1400		// The producer goes: the same handle now refuses rather than reporting
1401		// no demand, which is what stops a relay retrying a pull that has no
1402		// source left.
1403		drop(producer);
1404		assert!(matches!(watched.used().await, Err(Error::Dropped)));
1405		assert!(matches!(watched.unused().await, Err(Error::Dropped)));
1406	}
1407
1408	/// Subscribe and assert the result hasn't resolved yet (it stays pending until
1409	/// a publisher accepts). Returns the pending subscription to resolve after accepting.
1410	macro_rules! subscribe_pending {
1411		($consumer:expr, $name:expr) => {{
1412			let pending = $consumer.track($name).unwrap().subscribe(None);
1413			assert!(
1414				pending.poll_ok(&kio::Waiter::noop()).is_pending(),
1415				"subscribe should stay pending until the request is accepted"
1416			);
1417			pending
1418		}};
1419	}
1420
1421	#[tokio::test]
1422	async fn insert() {
1423		let mut producer = Info::new().produce();
1424
1425		// Create the track before any consumer exists.
1426		let mut track1 = producer.assert_create_track("track1", None);
1427		track1.append_group().unwrap();
1428
1429		let consumer = producer.consume();
1430
1431		// The track already exists, so subscribe resolves immediately.
1432		let mut track1_sub = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1433		track1_sub.assert_group();
1434
1435		let mut track2 = producer.assert_create_track("track2", None);
1436
1437		let consumer2 = producer.consume();
1438		let mut track2_consumer = consumer2.track("track2").unwrap().subscribe(None).await.unwrap();
1439		track2_consumer.assert_no_group();
1440
1441		track2.append_group().unwrap();
1442
1443		track2_consumer.assert_group();
1444	}
1445
1446	#[tokio::test]
1447	async fn closed() {
1448		let mut producer = Info::new().produce();
1449		let dynamic = producer.dynamic();
1450
1451		let consumer = producer.consume();
1452		consumer.assert_not_closed();
1453
1454		// Create a new track and insert it into the broadcast (resolves immediately).
1455		let track1 = producer.assert_create_track("track1", None);
1456		let mut track1c = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1457
1458		// A track nobody publishes stays pending until accepted.
1459		let track2_fut = subscribe_pending!(consumer, "track2");
1460
1461		// Dropping the last dynamic handler rejects pending requests, but must NOT
1462		// cascade to externally-owned tracks.
1463		drop(dynamic);
1464
1465		// track2 was a pending dynamic request, so its subscribe surfaces the rejection.
1466		assert!(track2_fut.await.is_err());
1467
1468		// track1's producer is held outside the broadcast, so it survives.
1469		assert!(!track1.is_closed());
1470		track1c.assert_not_closed();
1471	}
1472
1473	/// `closed()` reports the cause: the abort error, or `Dropped` for a finish or
1474	/// a dropped producer, with `is_finished` telling the latter two apart.
1475	#[tokio::test]
1476	async fn closed_cause() {
1477		// Abort: the error comes through, and it isn't a finish.
1478		let producer = Info::new().produce();
1479		let consumer = producer.consume();
1480		producer.abort(Error::Timeout).unwrap();
1481		assert!(matches!(consumer.closed().await, Error::Timeout));
1482		assert!(!consumer.is_finished());
1483
1484		// Finish: a deliberate clean end.
1485		let mut producer = Info::new().produce();
1486		let consumer = producer.consume();
1487		producer.finish();
1488		assert!(matches!(consumer.closed().await, Error::Dropped));
1489		assert!(consumer.is_finished());
1490
1491		// Plain drop: neither aborted nor finished.
1492		let producer = Info::new().produce();
1493		let consumer = producer.consume();
1494		// Deliberate for the test: exercises the accidental-drop path (warns).
1495		drop(producer);
1496		assert!(matches!(consumer.closed().await, Error::Dropped));
1497		assert!(!consumer.is_finished());
1498	}
1499
1500	#[tokio::test]
1501	async fn requests() {
1502		let mut producer = Info::new().produce().dynamic();
1503
1504		let consumer = producer.consume();
1505		let consumer2 = consumer.clone();
1506
1507		// Two subscribers to the same name coalesce into one request.
1508		let track1_fut = subscribe_pending!(consumer, "track1");
1509		let track2_fut = subscribe_pending!(consumer2, "track1");
1510
1511		// There should be exactly one request to serve.
1512		let request = producer.assert_request();
1513		producer.assert_no_request();
1514		assert_eq!(request.name(), "track1");
1515
1516		// Accept it, which resolves both waiting subscribers.
1517		let mut track3 = request.accept(None);
1518		let mut track1 = track1_fut.await.unwrap();
1519		let mut track2 = track2_fut.await.unwrap();
1520
1521		track1.assert_not_closed();
1522		track1.assert_is_clone(&track2);
1523		track3.subscribe(None).assert_is_clone(&track1);
1524
1525		// Append a group and make sure they all get it.
1526		track3.append_group().unwrap();
1527		track1.assert_group();
1528		track2.assert_group();
1529
1530		// A pending request is cancelled when the dynamic producer is dropped.
1531		let track4_fut = subscribe_pending!(consumer, "track2");
1532		drop(producer);
1533		assert!(track4_fut.await.is_err());
1534
1535		// With no dynamic producer left, requesting the handle fails outright.
1536		let track5 = consumer2.track("track3");
1537		assert!(track5.is_err(), "should have errored");
1538	}
1539
1540	#[tokio::test]
1541	async fn stale_producer() {
1542		let mut broadcast = Info::new().produce().dynamic();
1543		let consumer = broadcast.consume();
1544
1545		// Subscribe to a track and serve it.
1546		let track1_fut = subscribe_pending!(consumer, "track1");
1547		let mut producer1 = broadcast.assert_request().accept(None);
1548		let mut track1 = track1_fut.await.unwrap();
1549
1550		// Close the producer (simulating publisher disconnect).
1551		producer1.append_group().unwrap();
1552		producer1.finish().unwrap();
1553		drop(producer1);
1554
1555		// The consumer should see the track as closed.
1556		track1.assert_closed();
1557
1558		// Subscribe again to the same track: should get a NEW producer, not the stale one.
1559		let track2_fut = subscribe_pending!(consumer, "track1");
1560		let mut producer2 = broadcast.assert_request().accept(None);
1561		let mut track2 = track2_fut.await.unwrap();
1562		track2.assert_not_closed();
1563		track2.assert_not_clone(&track1);
1564
1565		// The new consumer should receive the new group.
1566		producer2.append_group().unwrap();
1567		track2.assert_group();
1568	}
1569
1570	#[tokio::test(start_paused = true)]
1571	async fn requested_unused() {
1572		let mut broadcast = Info::new().produce().dynamic();
1573		let bc = broadcast.consume();
1574
1575		// Subscribe to a track that doesn't exist yet, then serve it.
1576		let c1_fut = subscribe_pending!(bc, "unknown_track");
1577		let producer1 = broadcast.assert_request().accept(None);
1578		let consumer1 = c1_fut.await.unwrap();
1579
1580		// The producer should NOT be unused yet because there's a consumer.
1581		assert!(
1582			producer1.unused().now_or_never().is_none(),
1583			"track producer should be used"
1584		);
1585
1586		// A second subscriber reuses the live producer (fast path / dedup).
1587		let consumer2 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1588		consumer2.assert_is_clone(&consumer1);
1589
1590		drop(consumer1);
1591		assert!(
1592			producer1.unused().now_or_never().is_none(),
1593			"track producer should be used"
1594		);
1595
1596		drop(consumer2);
1597		assert!(
1598			producer1.unused().now_or_never().is_some(),
1599			"track producer should be unused after all consumers are dropped"
1600		);
1601
1602		// While the producer is still alive, re-subscribing to the same name reuses
1603		// it (no new request). This is what lets the relay linger upstream
1604		// subscriptions across transient consumer churn.
1605		let consumer3 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1606		consumer3.assert_is_clone(&producer1.subscribe(None));
1607		broadcast.assert_no_request();
1608		drop(consumer3);
1609
1610		// Aborting the producer closes its lookup entry; the next subscribe sees the
1611		// stale weak, evicts it, and creates a fresh request.
1612		producer1.abort(Error::Cancel).unwrap();
1613
1614		let c4_fut = subscribe_pending!(bc, "unknown_track");
1615		let producer2 = broadcast.assert_request().accept(None);
1616		let consumer4 = c4_fut.await.unwrap();
1617		drop(consumer4);
1618		assert!(
1619			producer2.unused().now_or_never().is_some(),
1620			"new track producer should be unused after its consumer is dropped"
1621		);
1622	}
1623
1624	/// Creating a track a consumer already requested fulfills that request: the
1625	/// waiting subscriber resolves against the created producer, and no handler
1626	/// ever sees the (now-taken) name. Without this the requester is stranded:
1627	/// the name exists the moment the track does, so the queue entry could
1628	/// never be served under it.
1629	#[tokio::test]
1630	async fn create_track_fulfills_queued_request() {
1631		let mut producer = Info::new().produce();
1632		let mut dynamic = producer.dynamic();
1633		let bc = dynamic.consume();
1634
1635		// Queue a request for a track that doesn't exist yet.
1636		let subscribing = subscribe_pending!(bc, "video");
1637
1638		// The producer creates the track before any handler drains the queue.
1639		let mut track = producer.create_track("video", None).unwrap();
1640		let mut sub = subscribing.await.expect("fulfilled by create_track");
1641
1642		// The fulfilled subscription is live against this very producer.
1643		track.append_group().unwrap();
1644		sub.recv_group().await.expect("recv").expect("group");
1645
1646		// The handler never sees the request; a fresh subscribe reuses the track.
1647		dynamic.assert_no_request();
1648		let again = bc.track("video").unwrap().subscribe(None).await.unwrap();
1649		again.assert_is_clone(&track.subscribe(None));
1650	}
1651
1652	// Cloning a `Consumer` resets its route cursor: a clone that inherited the
1653	// original's `route_seen` would skip the initial-value delivery that
1654	// `route_changed` promises.
1655	#[tokio::test]
1656	async fn route_clone_observes_current_route() {
1657		let mut producer = Info::new().produce();
1658		let mut consumer = producer.consume();
1659
1660		// Drain the initial route, then a change.
1661		consumer.route_changed().await.unwrap();
1662		let route = Route::new().with_cost(7);
1663		producer.set_route(route.clone()).unwrap();
1664		assert_eq!(consumer.route_changed().await.unwrap(), route);
1665
1666		// The original is fully drained: no update pending.
1667		assert!(consumer.route_changed().now_or_never().is_none());
1668
1669		// A clone starts fresh, yielding the current route immediately.
1670		let mut clone = consumer.clone();
1671		let seen = clone
1672			.route_changed()
1673			.now_or_never()
1674			.expect("clone should observe the current route immediately")
1675			.unwrap();
1676		assert_eq!(seen, route);
1677	}
1678
1679	// Cloning a `Dynamic` and dropping the clone must not flip the handler
1680	// count to zero. The relay's lite subscriber clones the
1681	// dynamic per spawned subscribe; if Clone skipped the increment, the
1682	// first finished subscribe would tear down the broadcast and any
1683	// follow-up `track` would return `NotFound`.
1684	#[tokio::test]
1685	async fn dynamic_clone_keeps_alive() {
1686		let broadcast = Info::new().produce().dynamic();
1687		let consumer = broadcast.consume();
1688
1689		let clone = broadcast.clone();
1690		drop(clone);
1691
1692		// Original handle is still live, so the request registers (stays pending)
1693		// instead of failing with NotFound.
1694		let _fut = subscribe_pending!(consumer, "track1");
1695	}
1696
1697	/// A reserved name nobody accepts is the parking case a publisher has to be able to
1698	/// end. Ending the broadcast is where it does: `Consumer::track` already answers
1699	/// `NotFound` for a name asked about after this point, so whoever asked earlier gets
1700	/// the same answer instead of waiting on info that can never arrive.
1701	#[tokio::test]
1702	async fn finish_resolves_a_reserved_name() {
1703		let mut producer = Info::new().produce();
1704		let consumer = producer.consume();
1705
1706		let _request = producer.reserve_track("track1").unwrap();
1707		let pending = subscribe_pending!(consumer, "track1");
1708
1709		producer.finish();
1710		assert!(matches!(pending.await, Err(Error::NotFound)));
1711	}
1712
1713	/// An abort says why the broadcast ended, and an unserved name resolves with that
1714	/// reason rather than a generic failure.
1715	#[tokio::test]
1716	async fn abort_resolves_a_reserved_name_with_its_reason() {
1717		let mut producer = Info::new().produce();
1718		let consumer = producer.consume();
1719
1720		let request = producer.reserve_track("track1").unwrap();
1721		let pending = subscribe_pending!(consumer, "track1");
1722
1723		producer.abort(Error::Cancel).unwrap();
1724		assert!(matches!(pending.await, Err(Error::Cancel)));
1725
1726		let track = request.accept(None);
1727		let mut subscriber = track.subscribe(None);
1728		assert!(matches!(subscriber.recv_group().await, Err(Error::Cancel)));
1729	}
1730
1731	/// A request still queued for a handler is the same parking case reached from the
1732	/// consumer side, so it ends the same way.
1733	#[tokio::test]
1734	async fn finish_resolves_a_queued_request() {
1735		let mut producer = Info::new().produce();
1736		let dynamic = producer.dynamic();
1737		let consumer = dynamic.consume();
1738
1739		let pending = subscribe_pending!(consumer, "track1");
1740
1741		producer.finish();
1742		assert!(matches!(pending.await, Err(Error::NotFound)));
1743		drop(dynamic);
1744	}
1745
1746	/// A request a handler already took parks the same way if the handler never answers
1747	/// it, so the sweep has to reach that one too.
1748	#[tokio::test]
1749	async fn finish_resolves_a_request_a_handler_never_answered() {
1750		let mut producer = Info::new().produce();
1751		let mut dynamic = producer.dynamic();
1752		let consumer = dynamic.consume();
1753
1754		let pending = subscribe_pending!(consumer, "track1");
1755		let _request = dynamic.requested_track().await.unwrap();
1756
1757		producer.finish();
1758		assert!(matches!(pending.await, Err(Error::NotFound)));
1759		drop(dynamic);
1760	}
1761
1762	/// A reverse fetch can install the track metadata before the live request is
1763	/// accepted, but it does not create a live publisher. Finishing the broadcast
1764	/// must still reject that name so an arrival-order subscriber does not park on
1765	/// backfill that is deliberately absent from its queue.
1766	#[tokio::test]
1767	async fn finish_resolves_an_unaccepted_track_with_fetched_info() {
1768		let mut producer = Info::new().produce();
1769		let consumer = producer.consume();
1770
1771		let request = producer.reserve_track("track1").unwrap();
1772		let dynamic = request.dynamic();
1773		let track = consumer.track("track1").unwrap();
1774		let pending_fetch = track.fetch_group(0, None);
1775		let fetch = dynamic.requested_group().await.unwrap();
1776		let mut group = fetch.accept(None).unwrap();
1777		group.finish().unwrap();
1778		pending_fetch.await.unwrap();
1779
1780		let mut subscriber = track.subscribe(None).await.unwrap();
1781		producer.finish();
1782		assert!(matches!(subscriber.recv_group().await, Err(Error::NotFound)));
1783
1784		let mut stale = request.accept(None);
1785		assert!(stale.append_group().is_err());
1786	}
1787
1788	/// Ending the broadcast doesn't cascade into a track someone is publishing: it keeps
1789	/// its cache and its publisher decides when it ends.
1790	#[tokio::test]
1791	async fn finish_spares_a_served_track() {
1792		let mut producer = Info::new().produce();
1793		let consumer = producer.consume();
1794
1795		let mut track = producer.create_track("track1", None).unwrap();
1796		let mut subscriber = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1797
1798		producer.finish();
1799
1800		track.append_group().unwrap();
1801		subscriber.assert_group();
1802		track.finish().unwrap();
1803	}
1804
1805	/// The publisher may still be holding the `track::Request` for a name the broadcast
1806	/// just gave up on. Accepting it afterwards must not resurrect the track, or a
1807	/// subscriber that was told `NotFound` could be contradicted by a later one.
1808	#[tokio::test]
1809	async fn finish_leaves_a_stale_reservation_inert() {
1810		let mut producer = Info::new().produce();
1811		let consumer = producer.consume();
1812
1813		let request = producer.reserve_track("track1").unwrap();
1814		let pending = subscribe_pending!(consumer, "track1");
1815
1816		producer.finish();
1817		assert!(matches!(pending.await, Err(Error::NotFound)));
1818
1819		let mut track = request.accept(None);
1820		assert!(track.append_group().is_err());
1821		let mut subscriber = track.subscribe(None);
1822		assert!(matches!(subscriber.recv_group().await, Err(Error::NotFound)));
1823		assert!(consumer.track("track1").is_err());
1824	}
1825
1826	/// Dropping a `track::Request` is not a verdict about the name, so it resolves as
1827	/// `Dropped` (a handler lost to a crashed publisher or a dead transport), never as
1828	/// `NotFound`. Only an explicit rejection may claim the track is absent.
1829	#[tokio::test]
1830	async fn dropping_a_reserved_request_resolves_dropped() {
1831		let mut producer = Info::new().produce();
1832		let consumer = producer.consume();
1833
1834		let request = producer.reserve_track("track1").unwrap();
1835		let pending = subscribe_pending!(consumer, "track1");
1836
1837		drop(request);
1838		assert!(matches!(pending.await, Err(Error::Dropped)));
1839		producer.finish();
1840	}
1841
1842	/// `track::Request::reject` carries its reason the same way, which is what lets a
1843	/// subscriber tell "no such track" from "the publisher went away".
1844	#[tokio::test]
1845	async fn rejecting_a_reserved_request_carries_the_reason() {
1846		let mut producer = Info::new().produce();
1847		let consumer = producer.consume();
1848
1849		let request = producer.reserve_track("track1").unwrap();
1850		let pending = subscribe_pending!(consumer, "track1");
1851
1852		request.reject(Error::NotFound);
1853		assert!(matches!(pending.await, Err(Error::NotFound)));
1854		producer.finish();
1855	}
1856}