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