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