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};
16
17use crate::Error;
18
19use super::{OriginList, Requests, WeakCache};
20
21/// A collection of media tracks that can be published and subscribed to.
22///
23/// Create via [`Info::produce`] to obtain both [`Producer`] and [`Consumer`] pair.
24/// This is the broadcast's static identity, fixed for its lifetime; the path it
25/// takes to get here is the dynamic [`Route`], observed via [`Consumer::route`].
26#[derive(Clone, Debug, Default)]
27#[non_exhaustive]
28pub struct Info {
29 /// The origin this broadcast belongs to (its identity, and the cache pool its
30 /// tracks and groups inherit). A track reaches its pool by walking up this link,
31 /// so the pool has a single home on the origin rather than being copied per
32 /// broadcast. Defaults to an unknown origin with an unbounded pool (a standalone
33 /// broadcast with no relay origin).
34 pub origin: super::origin::Info,
35}
36
37impl Info {
38 /// Create a new broadcast with default metadata.
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 /// Consume this [Info] to create a producer that carries its metadata.
44 ///
45 /// Keep the returned [`Producer`] alive for as long as the broadcast should stay
46 /// available, and end it with [`Producer::finish`]. See the note on [`Producer`].
47 pub fn produce(self) -> Producer {
48 Producer::new(self)
49 }
50}
51
52/// The path a broadcast takes to reach this origin, and how preferable it is.
53///
54/// Unlike [`Info`], the route is dynamic: it changes when the serving session fails
55/// over, the upstream topology shifts, or the publisher re-advertises itself.
56/// Publish a change with [`Producer::set_route`] and observe one with
57/// [`Consumer::route_changed`]; downstream sessions forward updates as a restart
58/// on the wire, so route churn never looks like a new broadcast.
59#[derive(Clone, Debug, Default, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct Route {
62 /// The chain of origins the broadcast has traversed, oldest first. Each relay
63 /// appends its own [`crate::Origin`] when forwarding; used for loop detection
64 /// and as the selection tie-break.
65 pub hops: OriginList,
66
67 /// The cost of pulling the broadcast via this route, accumulated per link:
68 /// lower wins, with ties broken by hop length and then a deterministic hash.
69 ///
70 /// The original publisher seeds it with its production cost (zero for a live
71 /// publish, something large for a standby that would have to start working,
72 /// like a cold transcoder), and each link adds its own configured price as
73 /// the announcement crosses it, so a route over a metered backbone ranks
74 /// worse than an equal-length one within a datacenter. The accumulation
75 /// restarts at zero at any node actively carrying the broadcast: those
76 /// upstream legs already exist and are not re-paid by one more subscriber,
77 /// so the sum is the cost of the transfers a subscription would newly cause.
78 ///
79 /// Carried on the wire from lite-06; older peers always report zero, leaving
80 /// the hop-count tie-break as the effective metric exactly as before.
81 pub cost: u64,
82
83 /// The cost as the announcing peer advertised it, before this link's charge
84 /// was added to [`Self::cost`]. Local bookkeeping, never forwarded: zero on a
85 /// chain of two or more hops means the announcing relay is actively carrying
86 /// the broadcast, which is what the origin's handover gate keys on.
87 pub(crate) advertised: u64,
88
89 /// Whether the broadcast should be announced: advertised to consumers via
90 /// [`crate::origin::Consumer::announced`] while this is the best route. A
91 /// non-announced broadcast stays reachable by exact path for subscribes and
92 /// fetches (e.g. serving cached or on-demand content), so toggling this via
93 /// [`Producer::set_route`] announces or unannounces without touching the
94 /// broadcast itself. Defaults to `false`.
95 pub announce: bool,
96}
97
98impl Route {
99 /// An unannounced direct route: no hops, best cost.
100 ///
101 /// The broadcast is reachable only by its exact path, so subscribers must already
102 /// know it exists. Use [`announced`](Self::announced) to advertise it instead.
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 /// An announced direct route: no hops, best cost.
108 ///
109 /// The broadcast is advertised to subscribers via
110 /// [`crate::origin::Consumer::announced`] while this is the best route, on top of
111 /// staying reachable by exact path. Use [`new`](Self::new) to keep it unadvertised.
112 pub fn announced() -> Self {
113 Self {
114 announce: true,
115 ..Self::default()
116 }
117 }
118
119 /// Append a hop to the chain, oldest first.
120 ///
121 /// Fails with [`crate::TooManyOrigins`] once the chain is full, the same limit
122 /// the wire enforces.
123 pub fn with_hop(mut self, origin: super::Origin) -> Result<Self, super::TooManyOrigins> {
124 self.hops.push(origin)?;
125 Ok(self)
126 }
127
128 /// Replace the hop chain.
129 pub fn with_hops(mut self, hops: OriginList) -> Self {
130 self.hops = hops;
131 self
132 }
133
134 /// Set the cost: lower wins among routes serving the same broadcast.
135 pub fn with_cost(mut self, cost: u64) -> Self {
136 self.cost = cost;
137 self
138 }
139
140 /// Set whether the broadcast is announced via this route.
141 pub fn with_announce(mut self, announce: bool) -> Self {
142 self.announce = announce;
143 self
144 }
145}
146
147#[derive(Default)]
148struct BroadcastState {
149 // Weak references for deduplication. Doesn't prevent track auto-close.
150 // Keyed by the track's shared `Arc<str>` name (the same Arc the handle holds).
151 // The cache reclaims closed entries incrementally on insert so a long-lived
152 // broadcast churning distinct track names stays bounded by the live count.
153 tracks: WeakCache<Arc<str>, track::TrackWeak>,
154
155 // Pending requests keyed by track name, coalescing concurrent `track()` calls
156 // and waiting for a dynamic handler to accept or deny them. A request leaves
157 // here once handed out (the handler caches it in `tracks`, so lookups keep
158 // coalescing onto it there).
159 requests: Requests<Arc<str>, track::Request>,
160
161 // Route-fed mode (a relay/origin "front"): tracks are spliced logical tracks
162 // joined across per-session tracks. `None` for an ordinary broadcast.
163 spliced: Option<SplicedState>,
164
165 // The path the broadcast currently takes to reach us, bumping `route_epoch`
166 // on every change so consumers can watch for updates.
167 route: Route,
168 route_epoch: u64,
169
170 // Set by an explicit `Producer::finish()` or `Producer::abort()` so `Drop` can
171 // tell a deliberate shutdown apart from a producer dropped by accident.
172 closing: bool,
173
174 // Set only by `Producer::finish()`: the broadcast ended deliberately, as
175 // opposed to aborting or losing its producer. The origin reads this to decide
176 // whether a detached source may linger for a replacement.
177 finished: bool,
178
179 // The error passed to `Producer::abort()`, reported by `Consumer::closed`.
180 // `None` for a finish or a dropped producer (reported as `Error::Dropped`).
181 abort: Option<Error>,
182}
183
184/// The spliced (route-fed) half of a broadcast: logical tracks that outlive any
185/// single session, plus the queue of tracks awaiting a serving route.
186#[derive(Default)]
187struct SplicedState {
188 // Logical tracks by name, owned strongly: they live as long as the broadcast
189 // (the origin's front), not as long as any consumer.
190 tracks: HashMap<Arc<str>, super::resume::Producer>,
191
192 // Names awaiting assignment to a route, in request order.
193 pending: VecDeque<Arc<str>>,
194}
195
196impl BroadcastState {
197 /// Insert a track weak handle into the lookup, returning an error if a live
198 /// track already holds the name. A closed entry under the name is reclaimed.
199 fn insert_track(&mut self, weak: track::TrackWeak) -> Result<(), Error> {
200 match self.tracks.insert(weak.name().clone(), weak) {
201 Some(_) => Err(Error::Duplicate),
202 None => Ok(()),
203 }
204 }
205
206 /// Live demand: a subscribed spliced track (route-fed broadcast), or a
207 /// pending request / consumed track (ordinary broadcast). See [`Demand`].
208 fn is_used(&self) -> bool {
209 if let Some(spliced) = &self.spliced {
210 return spliced.tracks.values().any(|track| track.is_used());
211 }
212 !self.requests.is_empty() || self.tracks.iter().any(|track| track.is_used())
213 }
214
215 /// Park `waiter` on every per-track channel feeding [`Self::is_used`]: the
216 /// consumer counts live on those channels, and their flips don't write this
217 /// state, so a watcher registered here alone would miss the edge. `want`
218 /// picks the direction; each channel only arms while its side is unmet.
219 fn register_demand(&self, waiter: &kio::Waiter, want: bool) {
220 if let Some(spliced) = &self.spliced {
221 for track in spliced.tracks.values() {
222 let _ = match want {
223 true => track.poll_used(waiter),
224 false => track.poll_unused(waiter),
225 };
226 }
227 return;
228 }
229 for track in self.tracks.iter() {
230 match want {
231 true => track.poll_used(waiter),
232 false => track.poll_unused(waiter),
233 }
234 }
235 }
236}
237
238/// Manages tracks within a broadcast.
239///
240/// Create tracks up front with [Self::create_track], reserve a name to fill in
241/// later with [Self::reserve_track], or handle on-demand consumer requests via
242/// [Self::dynamic].
243///
244/// # Lifetime
245///
246/// **You must keep this producer alive for as long as the broadcast should stay
247/// available.** A broadcast lives as long as at least one [`Producer`] exists;
248/// children do *not* keep it alive (cloning a [`Consumer`] or holding a
249/// [`track::Producer`] does nothing for the broadcast's lifetime). When the last
250/// producer goes away every consumer observes [`Error::Dropped`].
251///
252/// End the broadcast with [`Self::finish`] rather than dropping it. Dropping is an
253/// easy footgun in garbage-collected bindings (Go, Python, ...), where the handle
254/// can be collected the moment it falls out of scope even while you are still
255/// publishing, tearing the stream down mid-broadcast. Dropping the last producer
256/// without [`Self::finish`] logs a warning.
257#[derive(Clone)]
258pub struct Producer {
259 // Held behind an Arc so each track born from this broadcast can inherit a shared
260 // handle (threaded down by [`Self::create_track`] / [`Self::reserve_track`]).
261 info: Arc<Info>,
262
263 // Broadcast liveness, shared with every `Dynamic`. Consumers watch it (read-only)
264 // for close; the guard ends the broadcast when the last of those handles drops.
265 alive: Arc<Alive>,
266
267 // Track registry plus the dynamic request queue, mutated by producers and
268 // consumers alike under one lock.
269 state: kio::Shared<BroadcastState>,
270
271 // Ingress stats scope, set by a tagged `origin::Producer` at
272 // `create_broadcast`. Inherited by the tracks this producer creates. Empty
273 // (no-op) for an untagged broadcast.
274 stats: stats::Scope,
275}
276
277impl Producer {
278 /// Create a producer for the given broadcast metadata. Prefer [`Info::produce`].
279 pub fn new(info: Info) -> Self {
280 let state = kio::Shared::<BroadcastState>::default();
281 Self {
282 info: Arc::new(info),
283 alive: Alive::new(state.clone()),
284 state,
285 stats: stats::Scope::default(),
286 }
287 }
288
289 /// Attach an ingress stats scope, inherited by the tracks created on this
290 /// broadcast. Set by a tagged `origin::Producer` at `create_broadcast`.
291 pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
292 self.stats = scope;
293 self
294 }
295
296 /// Create a route-fed (spliced) broadcast: consumer track lookups mint logical
297 /// tracks that are spliced across per-session tracks, queued for a route to
298 /// serve. Used by the origin for broadcasts reached over the network.
299 pub(crate) fn new_spliced(info: Info) -> Self {
300 let state = kio::Shared::new(BroadcastState {
301 spliced: Some(SplicedState::default()),
302 ..Default::default()
303 });
304 Self {
305 info: Arc::new(info),
306 alive: Alive::new(state.clone()),
307 state,
308 // The origin-owned spliced broadcast stays untagged: egress attribution is
309 // applied when a tagged `origin::Consumer` hands the consumer out.
310 stats: stats::Scope::default(),
311 }
312 }
313
314 /// The broadcast's static metadata, fixed when it was created.
315 pub fn info(&self) -> &Info {
316 &self.info
317 }
318
319 /// A watch-only handle to the broadcast's demand. See [`Demand`].
320 pub fn demand(&self) -> Demand {
321 Demand {
322 alive: self.alive.token.consume().weak(),
323 state: self.state.clone(),
324 }
325 }
326
327 /// Remove a track from the lookup.
328 pub fn remove_track(&mut self, name: &str) -> Result<(), Error> {
329 self.state.lock().tracks.remove(name).ok_or(Error::NotFound)?;
330 Ok(())
331 }
332
333 /// Produce a new track and insert it into the broadcast.
334 ///
335 /// Pass a name and an optional [`track::Info`], so a bare name works:
336 /// `create_track("video", None)`.
337 pub fn create_track(
338 &mut self,
339 name: impl Into<Arc<str>>,
340 info: impl Into<Option<track::Info>>,
341 ) -> Result<track::Producer, Error> {
342 let info = info.into().unwrap_or_default();
343 let track = track::Producer::new(self.info.clone(), name, info).with_stats(self.stats.clone());
344 self.state.lock().insert_track(track.weak())?;
345 Ok(track)
346 }
347
348 /// Reserve a track by name without finalizing its [`track::Info`].
349 ///
350 /// Returns a [`track::Request`] already discoverable by consumers; call
351 /// [`track::Request::accept`] to set its info and start producing. Use this when
352 /// the producer can't pick the track's properties (e.g. timescale) until it has
353 /// inspected the media, the same shape as a consumer-driven
354 /// [`Dynamic::requested_track`].
355 pub fn reserve_track(&mut self, name: impl Into<Arc<str>>) -> Result<track::Request, Error> {
356 let request = track::Request::new(self.info.clone(), name).with_stats(self.stats.clone());
357 self.state.lock().insert_track(request.weak())?;
358 Ok(request)
359 }
360
361 /// Create a track with a unique name using the given suffix.
362 ///
363 /// Generates names like `0{suffix}`, `1{suffix}`, etc. and picks the first
364 /// one not already used in this broadcast.
365 pub fn unique_track(
366 &mut self,
367 suffix: &str,
368 info: impl Into<Option<track::Info>>,
369 ) -> Result<track::Producer, Error> {
370 let name = self.unique_name(suffix);
371 self.create_track(name, info)
372 }
373
374 /// Generate a unique track name from a suffix without creating the track.
375 ///
376 /// Returns a fresh name like `0{suffix}`, `1{suffix}`, etc. Use this when
377 /// you need to set non-default Track properties (e.g. `with_timescale`,
378 /// `with_latency_max`) before handing the Track to [`Self::create_track`].
379 pub fn unique_name(&self, suffix: &str) -> String {
380 let state = self.state.read();
381 (0u16..)
382 .map(|i| format!("{i}{suffix}"))
383 .find(|name| !state.tracks.contains_key(name.as_str()))
384 .expect("u16 namespace exhausted; wow")
385 }
386
387 /// Create a dynamic producer that handles on-demand track requests from consumers.
388 pub fn dynamic(&self) -> Dynamic {
389 Dynamic::new(
390 self.info.clone(),
391 self.alive.clone(),
392 self.state.clone(),
393 self.stats.clone(),
394 )
395 }
396
397 /// Set the broadcast's [`Route`]: the hop chain and cost it advertises.
398 ///
399 /// Call this when the path to the content changes (an upstream failover) or the
400 /// publisher's preference changes (e.g. a transcoder warming up lowers its
401 /// cost). Consumers observe the change via [`Consumer::route_changed`] and
402 /// sessions forward it downstream as a restart, never as a new broadcast.
403 /// Setting the current route again is a no-op.
404 pub fn set_route(&mut self, route: Route) -> Result<(), Error> {
405 let mut state = self.state.lock();
406 if state.route == route {
407 return Ok(());
408 }
409 state.route = route;
410 state.route_epoch += 1;
411 Ok(())
412 }
413
414 /// Poll for the next spliced track awaiting a serving route, returning its name
415 /// and logical producer. Route-fed broadcasts only.
416 pub(crate) fn poll_spliced_assigned(&self, waiter: &kio::Waiter) -> Poll<(Arc<str>, super::resume::Producer)> {
417 let mut state = ready!(self.state.poll(waiter, |state| {
418 match &state.spliced {
419 Some(spliced) if !spliced.pending.is_empty() => Poll::Ready(()),
420 _ => Poll::Pending,
421 }
422 }));
423
424 let spliced = state.spliced.as_mut().expect("predicate guaranteed spliced");
425 let name = spliced.pending.pop_front().expect("predicate guaranteed a request");
426 let producer = spliced.tracks.get(&name).expect("pending name without a track").clone();
427 Poll::Ready((name, producer))
428 }
429
430 /// Abort every spliced track, releasing their subscribers with `err`. Called
431 /// when the broadcast closes for good.
432 pub(crate) fn abort_spliced(&self, err: Error) {
433 let mut state = self.state.lock();
434 if let Some(spliced) = state.spliced.as_mut() {
435 spliced.pending.clear();
436 for producer in spliced.tracks.values_mut() {
437 let _ = producer.abort(err.clone());
438 }
439 }
440 }
441
442 /// Create a consumer that can subscribe to tracks in this broadcast.
443 pub fn consume(&self) -> Consumer {
444 Consumer {
445 info: self.info.clone(),
446 alive: self.alive.token.consume(),
447 state: self.state.clone(),
448 route_seen: None,
449 stats: stats::Scope::default(),
450 }
451 }
452
453 /// Cleanly finish the broadcast once you are done publishing.
454 ///
455 /// Marks the broadcast as deliberately finished so consumers observe a normal
456 /// end. Prefer this over dropping the producer: an accidental drop (see the note
457 /// on [`Producer`]) logs a warning, whereas `finish()` is silent.
458 ///
459 /// Ends the broadcast outright: consumers observe a normal end immediately and no
460 /// new tracks are served, whether or not other producer clones are still alive.
461 /// Existing tracks stay readable so consumers can drain what they already have.
462 ///
463 /// Borrows rather than consumes, matching [`track::Producer::finish`]. Finishing
464 /// declares the end, so it must not depend on the caller also surrendering the
465 /// handle.
466 pub fn finish(&mut self) {
467 {
468 let mut state = self.state.lock();
469 state.closing = true;
470 state.finished = true;
471 }
472 // Ending the broadcast is what consumers wait on, so signal it here rather
473 // than leaving it to the last handle drop.
474 let _ = self.alive.token.close();
475 }
476
477 /// Abort the broadcast, ending it for consumers with `err`.
478 ///
479 /// Like [`finish`](Self::finish) the end is immediate, whether or not other
480 /// producer clones are still alive, and existing tracks stay readable so
481 /// consumers can drain what they already have (an abort does not cascade into
482 /// the tracks). Unlike a finish, consumers observe `err` from
483 /// [`Consumer::closed`], and an origin treats the source as ungracefully lost,
484 /// so the path may linger for a replacement (see
485 /// [`origin::Info::linger`](crate::origin::Info::linger)).
486 ///
487 /// Consumes the producer: an abort is terminal. Errors if the broadcast was
488 /// already finished or aborted.
489 pub fn abort(self, err: Error) -> Result<(), Error> {
490 {
491 let mut state = self.state.lock();
492 if state.closing {
493 return Err(Error::Closed);
494 }
495 state.closing = true;
496 state.abort = Some(err);
497 }
498 let _ = self.alive.token.close();
499 Ok(())
500 }
501
502 /// Return true if this is the same broadcast instance.
503 pub fn is_clone(&self, other: &Self) -> bool {
504 self.state.same_channel(&other.state)
505 }
506}
507
508/// Ends the broadcast when the last [`Producer`] or [`Dynamic`] drops, closing the
509/// liveness channel every [`Consumer`] watches.
510///
511/// A refcount rather than a "am I the last one?" check inside `Drop`: that answer is
512/// a snapshot, and acting on it is exactly what invalidates it.
513struct Alive {
514 token: kio::Producer<()>,
515 state: kio::Shared<BroadcastState>,
516}
517
518impl Alive {
519 fn new(state: kio::Shared<BroadcastState>) -> Arc<Self> {
520 Arc::new(Self {
521 token: kio::Producer::default(),
522 state,
523 })
524 }
525}
526
527impl Drop for Alive {
528 fn drop(&mut self) {
529 // Warn if the last exit wasn't an explicit finish(), since consumers will
530 // then see Error::Dropped (classically a GC-collected handle in a language
531 // binding that tears the stream down mid-publish).
532 if !self.state.read().closing {
533 tracing::warn!(
534 "broadcast::Producer dropped without finish(). Keep the producer alive while publishing, then call finish()."
535 );
536 }
537 }
538}
539
540#[cfg(test)]
541#[allow(missing_docs)] // test-only assertion helpers
542impl Producer {
543 pub fn assert_create_track(
544 &mut self,
545 name: impl Into<Arc<str>>,
546 info: impl Into<Option<track::Info>>,
547 ) -> track::Producer {
548 self.create_track(name, info).expect("should not have errored")
549 }
550}
551
552/// A session-owned handle to a source broadcast created via
553/// [`crate::origin::Producer::create_broadcast`]: [`Self::finish`] ends it
554/// deliberately, while dropping the guard aborts it as [`Error::Dropped`] (a dead
555/// session), letting the origin linger the path for a reconnect. Shared by the
556/// lite and IETF subscribers so the drop-vs-finish contract lives in one place.
557pub(crate) struct SourceGuard {
558 // `Option` so `finish` can consume the producer while `Drop` aborts it.
559 producer: Option<Producer>,
560}
561
562impl SourceGuard {
563 pub fn new(producer: Producer) -> Self {
564 Self {
565 producer: Some(producer),
566 }
567 }
568
569 /// A clone of the guarded producer.
570 pub fn producer(&self) -> Producer {
571 self.producer.clone().expect("guard holds a producer until finished")
572 }
573
574 /// End the source deliberately: the origin detaches it immediately,
575 /// unannouncing the path if it was the last.
576 pub fn finish(mut self) {
577 if let Some(mut producer) = self.producer.take() {
578 producer.finish();
579 }
580 }
581
582 /// Update the source's advertised route in place.
583 pub fn set_route(&mut self, route: Route) {
584 if let Some(producer) = &mut self.producer {
585 let _ = producer.set_route(route);
586 }
587 }
588}
589
590impl Drop for SourceGuard {
591 fn drop(&mut self) {
592 if let Some(producer) = self.producer.take() {
593 let _ = producer.abort(Error::Dropped);
594 }
595 }
596}
597
598/// Handles on-demand track creation for a broadcast.
599///
600/// When a consumer requests a track that doesn't exist, the dynamic producer
601/// picks up the request via [`Self::requested_track`] and either
602/// [`track::Request::accept`]s it with a concrete [`track::Info`] or
603/// [`track::Request::reject`]s it. Dropped when no longer needed; pending requests
604/// are automatically aborted.
605pub struct Dynamic {
606 info: Arc<Info>,
607 // Keeps the broadcast alive while a handler exists (mirrors a producer).
608 alive: Arc<Alive>,
609 state: kio::Shared<BroadcastState>,
610 // Ingress stats scope, applied to the tracks this handler serves. Empty (no-op)
611 // for an untagged broadcast.
612 stats: stats::Scope,
613}
614
615impl Clone for Dynamic {
616 fn clone(&self) -> Self {
617 // Mirror `new`: count each live handle. Without this, deriving Clone would
618 // let `Drop` decrement past `new`'s single increment and prematurely flip
619 // the handler count to zero, causing future `track` calls to return `NotFound`.
620 self.state.lock().requests.add_handler();
621
622 Self {
623 info: self.info.clone(),
624 alive: self.alive.clone(),
625 state: self.state.clone(),
626 stats: self.stats.clone(),
627 }
628 }
629}
630
631impl Dynamic {
632 fn new(info: Arc<Info>, alive: Arc<Alive>, state: kio::Shared<BroadcastState>, stats: stats::Scope) -> Self {
633 state.lock().requests.add_handler();
634
635 Self {
636 info,
637 alive,
638 state,
639 stats,
640 }
641 }
642
643 /// The broadcast's static metadata, fixed when it was created.
644 pub fn info(&self) -> &Info {
645 &self.info
646 }
647
648 /// Poll for the next consumer-requested track, without blocking.
649 ///
650 /// Returns [`Error::Closed`] once the broadcast was deliberately ended
651 /// ([`Producer::finish`] or aborted), so a serving loop knows to stop and
652 /// release its handle.
653 pub fn poll_requested_track(&mut self, waiter: &kio::Waiter) -> Poll<Result<track::Request, Error>> {
654 let mut state = ready!(self.state.poll(waiter, |state| {
655 if state.requests.has_queued() || state.closing {
656 Poll::Ready(())
657 } else {
658 Poll::Pending
659 }
660 }));
661
662 if state.closing && !state.requests.has_queued() {
663 return Poll::Ready(Err(Error::Closed));
664 }
665
666 let name = state.requests.pop().expect("predicate guaranteed a request");
667 let pending = state.requests.remove(&name).expect("popped key must be pending");
668 // Cache the served track so concurrent lookups coalesce onto it. If a live track already
669 // holds the name (a publish raced the request), `insert` keeps it rather than shadowing it.
670 let _ = state.tracks.insert(name, pending.weak());
671 // Attribute the served track to this broadcast's ingress scope (no-op untagged).
672 Poll::Ready(Ok(pending.with_stats(self.stats.clone())))
673 }
674
675 /// Block until a consumer requests a track, returning a [`track::Request`] to serve.
676 pub async fn requested_track(&mut self) -> Result<track::Request, Error> {
677 kio::wait(|waiter| self.poll_requested_track(waiter)).await
678 }
679
680 /// Create a consumer that can subscribe to tracks in this broadcast.
681 pub fn consume(&self) -> Consumer {
682 Consumer {
683 info: self.info.clone(),
684 alive: self.alive.token.consume(),
685 state: self.state.clone(),
686 route_seen: None,
687 stats: stats::Scope::default(),
688 }
689 }
690
691 /// Block until the broadcast is closed, by [`Producer::finish`],
692 /// [`Producer::abort`], or every producer dropping, returning the cause.
693 pub async fn closed(&self) -> Error {
694 kio::wait(|waiter| self.poll_closed(waiter)).await
695 }
696
697 /// Poll until the broadcast closes; ready with the cause: the error passed to
698 /// [`Producer::abort`], or [`Error::Dropped`] for a [`Producer::finish`] or a
699 /// dropped producer (check [`Consumer::is_finished`] to tell those apart).
700 pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
701 ready!(self.alive.token.poll_closed(waiter));
702 Poll::Ready(self.state.read().abort.clone().unwrap_or(Error::Dropped))
703 }
704
705 /// Return true if this is the same broadcast instance.
706 pub fn is_clone(&self, other: &Self) -> bool {
707 self.state.same_channel(&other.state)
708 }
709}
710
711impl Drop for Dynamic {
712 fn drop(&mut self) {
713 // Decrement and reject under one lock, so a `track` call that saw a live
714 // handler through the same lock can't slip a request past the rejection.
715 let mut state = self.state.lock();
716 if state.requests.remove_handler() {
717 // No handlers left to fulfill pending requests; reject them so consumers
718 // don't block forever on tracks nobody will serve.
719 for request in state.requests.drain_queued() {
720 request.reject(Error::Dropped);
721 }
722 }
723 }
724}
725
726#[cfg(test)]
727use futures::FutureExt;
728
729#[cfg(test)]
730#[allow(missing_docs)] // test-only assertion helpers
731impl Dynamic {
732 pub fn assert_request(&mut self) -> track::Request {
733 self.requested_track()
734 .now_or_never()
735 .expect("should not have blocked")
736 .expect("should not have errored")
737 }
738
739 pub fn assert_no_request(&mut self) {
740 assert!(self.requested_track().now_or_never().is_none(), "should have blocked");
741 }
742}
743
744/// Subscribe to arbitrary broadcast/tracks.
745pub struct Consumer {
746 info: Arc<Info>,
747 // Broadcast liveness (read-only): watched for close.
748 alive: kio::Consumer<()>,
749 // Track registry plus request queue; `track()` reads the registry and enqueues requests.
750 state: kio::Shared<BroadcastState>,
751 // The route epoch last yielded by `route_changed`, so each consumer clone
752 // observes the current route first and every change after it exactly once.
753 route_seen: Option<u64>,
754 // Egress stats scope, set by a tagged `origin::Consumer` at the broadcast
755 // handoff. Inherited by the tracks subscribed through this handle. Empty (no-op)
756 // for an untagged broadcast.
757 stats: stats::Scope,
758}
759
760impl Clone for Consumer {
761 fn clone(&self) -> Self {
762 Self {
763 info: self.info.clone(),
764 alive: self.alive.clone(),
765 state: self.state.clone(),
766 // Reset the cursor so the clone observes the current route first,
767 // even if the original already drained `route_changed`.
768 route_seen: None,
769 stats: self.stats.clone(),
770 }
771 }
772}
773
774impl Consumer {
775 /// Attach an egress stats scope, inherited by the tracks subscribed through this
776 /// handle. Set by a tagged `origin::Consumer` at the broadcast handoff.
777 pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
778 self.stats = scope;
779 self
780 }
781
782 /// The broadcast's static metadata, fixed when it was created.
783 pub fn info(&self) -> &Info {
784 &self.info
785 }
786
787 /// The [`Route`] the broadcast currently takes to reach this origin.
788 pub fn route(&self) -> Route {
789 self.state.read().route.clone()
790 }
791
792 /// Poll for a route change. See [`Self::route_changed`].
793 pub fn poll_route_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Route, Error>> {
794 let seen = self.route_seen;
795 if let Poll::Ready(state) = self.state.poll(waiter, |state| {
796 if seen != Some(state.route_epoch) {
797 Poll::Ready(())
798 } else {
799 Poll::Pending
800 }
801 }) {
802 self.route_seen = Some(state.route_epoch);
803 return Poll::Ready(Ok(state.route.clone()));
804 }
805 // No pending change: surface the broadcast's end instead of parking forever.
806 ready!(self.alive.poll_closed(waiter));
807 Poll::Ready(Err(Error::Dropped))
808 }
809
810 /// Wait for the broadcast's [`Route`] to change.
811 ///
812 /// The first call returns the current route immediately; each later call blocks
813 /// until it changes again, so a loop observes the initial value followed by
814 /// every update. Returns [`Error::Dropped`] once every producer is gone.
815 pub async fn route_changed(&mut self) -> Result<Route, Error> {
816 kio::wait(|waiter| self.poll_route_changed(waiter)).await
817 }
818
819 /// Get a handle to a track on this broadcast.
820 pub fn track(&self, name: &str) -> Result<track::Consumer, Error> {
821 // Tag the resolved track with this broadcast's egress scope so its
822 // subscriptions, fetches, and groups are attributed to the same broadcast.
823 self.track_inner(name).map(|track| track.with_stats(self.stats.clone()))
824 }
825
826 fn track_inner(&self, name: &str) -> Result<track::Consumer, Error> {
827 // A closed broadcast (every producer and handler gone) serves nothing.
828 if self.is_closed() {
829 return Err(Error::Dropped);
830 }
831
832 let mut state = self.state.lock();
833
834 // A route-fed broadcast mints spliced logical tracks: they outlive any
835 // session, and a route is asked (via the pending queue) to start serving.
836 if let Some(spliced) = state.spliced.as_mut() {
837 if let Some(producer) = spliced.tracks.get(name) {
838 return Ok(track::Consumer::spliced(name.into(), producer.consume()));
839 }
840 let name: Arc<str> = name.into();
841 let producer = super::resume::Producer::new();
842 let consumer = producer.consume();
843 spliced.tracks.insert(name.clone(), producer);
844 spliced.pending.push_back(name.clone());
845 return Ok(track::Consumer::spliced(name, consumer));
846 }
847
848 // Reuse a live producer if one is already publishing the track. `get` drops a
849 // closed entry and returns `None`, so we fall through to a fresh request.
850 if let Some(weak) = state.tracks.get(name) {
851 return Ok(weak.consume());
852 }
853
854 if let Some(pending) = state.requests.join(name) {
855 // Coalesce onto a queued request for the same name.
856 return Ok(pending.consume());
857 }
858
859 // A deliberately-ended broadcast serves nothing new; existing tracks above
860 // stay readable so consumers can drain the cache.
861 if state.closing {
862 return Err(Error::NotFound);
863 }
864
865 // Allocate the name once and share the same Arc across the request, the
866 // requests map, and the FIFO order. The request inherits the broadcast's
867 // cache pool through its `Arc<Info>`, same as a producer-created track.
868 let name: Arc<str> = name.into();
869 let request = track::Request::new(self.info.clone(), name.clone());
870 let consumer = request.consume();
871
872 // With no handler alive to serve it, the request is dropped: `NotFound` beats
873 // handing back a consumer that would only resolve `Dropped`.
874 if state.requests.insert(name, request).is_err() {
875 return Err(Error::NotFound);
876 }
877
878 Ok(consumer)
879 }
880
881 /// A watch-only handle to the broadcast's demand. See [`Demand`].
882 pub(crate) fn demand(&self) -> Demand {
883 Demand {
884 alive: self.alive.weak(),
885 state: self.state.clone(),
886 }
887 }
888
889 /// Block until the broadcast is closed, by [`Producer::finish`],
890 /// [`Producer::abort`], or every producer dropping, and return the cause.
891 ///
892 /// Returns the error passed to [`Producer::abort`], or [`Error::Dropped`] for a
893 /// [`Producer::finish`] or a dropped producer (check [`Self::is_finished`] to
894 /// tell those apart).
895 pub async fn closed(&self) -> Error {
896 self.alive.closed().await;
897 self.state.read().abort.clone().unwrap_or(Error::Dropped)
898 }
899
900 /// Returns true if every [`Producer`] has been dropped.
901 pub fn is_closed(&self) -> bool {
902 self.alive.is_closed()
903 }
904
905 /// Whether the broadcast is on its way out: deliberately ended (finish/abort
906 /// marked, even while handles remain) or already fully closed. The origin's
907 /// dispatcher treats a rejection from such a source as imminent detach rather
908 /// than a strike.
909 pub(crate) fn is_closing(&self) -> bool {
910 self.is_closed() || self.state.read().closing
911 }
912
913 /// Whether the broadcast ended via a deliberate [`Producer::finish`], as opposed
914 /// to aborting or losing its producer. `false` while the broadcast is still live;
915 /// an origin uses this to close a front immediately on a deliberate end instead
916 /// of lingering for a replacement.
917 pub fn is_finished(&self) -> bool {
918 self.state.read().finished
919 }
920
921 /// Register a [`kio::Waiter`] that fires when the broadcast closes.
922 ///
923 /// Returns [`Poll::Ready`] if already closed, otherwise [`Poll::Pending`] after
924 /// arming the waiter. Useful for composing close-detection into a larger poll
925 /// without spawning a task per broadcast.
926 pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
927 self.alive.poll_closed(waiter)
928 }
929
930 /// Check if this is the exact same instance of a broadcast.
931 pub fn is_clone(&self, other: &Self) -> bool {
932 self.state.same_channel(&other.state)
933 }
934
935 /// Create a weak reference that doesn't keep the broadcast alive.
936 ///
937 /// Used to deduplicate dynamically-served broadcasts in the origin: a live weak yields
938 /// a shared clone, a closed one is discarded so the next request re-serves.
939 pub(crate) fn weak(&self) -> WeakConsumer {
940 WeakConsumer {
941 info: self.info.clone(),
942 alive: self.alive.weak(),
943 state: self.state.clone(),
944 }
945 }
946}
947
948/// A weak reference to a broadcast that doesn't prevent it from closing.
949///
950/// Mirrors [`track::TrackWeak`]: held by the origin's dynamic cache to share one
951/// dynamically-served broadcast across repeat requests without pinning it alive.
952/// Only the `alive` handle needs to be weak; a [`kio::Shared`] carries no liveness,
953/// so holding the state outright pins nothing.
954#[derive(Clone)]
955pub(crate) struct WeakConsumer {
956 info: Arc<Info>,
957 alive: kio::ConsumerWeak<()>,
958 state: kio::Shared<BroadcastState>,
959}
960
961impl WeakConsumer {
962 /// Upgrade to a full [`Consumer`] sharing the same broadcast state.
963 pub fn consume(&self) -> Consumer {
964 Consumer {
965 info: self.info.clone(),
966 alive: self.alive.consume(),
967 state: self.state.clone(),
968 route_seen: None,
969 stats: stats::Scope::default(),
970 }
971 }
972}
973
974impl super::WeakEntry for WeakConsumer {
975 fn is_closed(&self) -> bool {
976 self.alive.is_closed()
977 }
978
979 fn same_channel(&self, other: &Self) -> bool {
980 self.state.same_channel(&other.state)
981 }
982}
983
984/// A cloneable, watch-only handle to a broadcast's subscriber demand.
985///
986/// Obtained from [`Producer::demand`]; the broadcast-level sibling of
987/// [`track::Demand`](crate::track::Demand). Demand means live interest in the
988/// broadcast's content: a subscribed spliced track on a route-fed broadcast, or
989/// a pending track request / a consumed track on an ordinary one. A publisher
990/// uses it to run expensive work only while someone is watching, and routing
991/// uses it to advertise a warm copy at zero cost.
992///
993/// It's a weak handle: it neither keeps the broadcast alive nor counts as
994/// demand itself. Once every producer is gone, [`used`](Self::used) /
995/// [`unused`](Self::unused) return [`Error::Dropped`].
996#[derive(Clone)]
997pub struct Demand {
998 alive: kio::ConsumerWeak<()>,
999 state: kio::Shared<BroadcastState>,
1000}
1001
1002impl Demand {
1003 /// Whether the broadcast has live demand right now.
1004 ///
1005 /// A point-in-time snapshot with no registration; use [`Self::used`] /
1006 /// [`Self::unused`] (or their `poll_*` forms) to wait for the edge.
1007 pub fn is_used(&self) -> bool {
1008 self.state.read().is_used()
1009 }
1010
1011 /// Block until the broadcast has demand. Resolves immediately if it already
1012 /// does; returns [`Error::Dropped`] once every producer is gone.
1013 pub async fn used(&self) -> Result<(), Error> {
1014 kio::wait(|waiter| self.poll_used(waiter)).await
1015 }
1016
1017 /// Block until the broadcast has no demand. Resolves immediately if it has
1018 /// none; returns [`Error::Dropped`] once every producer is gone.
1019 pub async fn unused(&self) -> Result<(), Error> {
1020 kio::wait(|waiter| self.poll_unused(waiter)).await
1021 }
1022
1023 /// Poll-based variant of [`Self::used`].
1024 pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1025 self.poll_demand(waiter, true)
1026 }
1027
1028 /// Poll-based variant of [`Self::unused`].
1029 pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1030 self.poll_demand(waiter, false)
1031 }
1032
1033 fn poll_demand(&self, waiter: &kio::Waiter, want: bool) -> Poll<Result<(), Error>> {
1034 // Closure is checked first, matching `track::Demand`: a dead broadcast
1035 // reports Dropped rather than pretending to answer.
1036 if self.alive.poll_closed(waiter).is_ready() {
1037 return Poll::Ready(Err(Error::Dropped));
1038 }
1039 let ready = self.state.poll(waiter, |state| {
1040 // The consumer counts live on the per-track channels, whose flips
1041 // don't write this state: park on those channels too so the edge
1042 // wakes us, then recompute here.
1043 state.register_demand(waiter, want);
1044 match state.is_used() == want {
1045 true => Poll::Ready(()),
1046 false => Poll::Pending,
1047 }
1048 });
1049 match ready {
1050 Poll::Ready(_) => Poll::Ready(Ok(())),
1051 Poll::Pending => Poll::Pending,
1052 }
1053 }
1054}
1055
1056#[cfg(test)]
1057#[allow(missing_docs)] // test-only assertion helpers
1058impl Consumer {
1059 pub fn assert_not_closed(&self) {
1060 assert!(self.closed().now_or_never().is_none(), "should not be closed");
1061 }
1062
1063 pub fn assert_closed(&self) {
1064 assert!(self.closed().now_or_never().is_some(), "should be closed");
1065 }
1066}
1067
1068#[cfg(test)]
1069mod test {
1070 use super::*;
1071
1072 /// Await with a timeout so a missed demand wake fails the test instead of
1073 /// hanging it (time is paused, so the timeout fires instantly when idle).
1074 async fn expect<T>(fut: impl Future<Output = T>) -> T {
1075 tokio::time::timeout(std::time::Duration::from_secs(1), fut)
1076 .await
1077 .expect("timed out waiting for a demand edge")
1078 }
1079
1080 /// Demand on an ordinary broadcast tracks subscriber interest, not
1081 /// production: a live track producer alone is unused, a consumed track is
1082 /// used, and both edges wake parked waiters.
1083 #[tokio::test]
1084 async fn demand_ordinary() {
1085 tokio::time::pause();
1086
1087 let mut producer = Info::new().produce();
1088 let consumer = producer.consume();
1089 let demand = producer.demand();
1090
1091 // No demand yet; `unused` resolves immediately.
1092 assert!(!demand.is_used());
1093 demand.unused().await.unwrap();
1094
1095 // Producing alone is not demand.
1096 let _track = producer.create_track("a", None).unwrap();
1097 assert!(!demand.is_used());
1098
1099 // A consumer appearing wakes a parked `used`.
1100 let (used, handle) = tokio::join!(expect(demand.used()), async { consumer.track("a").unwrap() });
1101 used.unwrap();
1102 assert!(demand.is_used());
1103
1104 // The last consumer dropping wakes a parked `unused`.
1105 let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(handle) });
1106 unused.unwrap();
1107 assert!(!demand.is_used());
1108
1109 // Every producer gone: both edges report the closure.
1110 producer.finish();
1111 assert!(matches!(demand.used().await, Err(Error::Dropped)));
1112 assert!(matches!(demand.unused().await, Err(Error::Dropped)));
1113 }
1114
1115 /// Demand on a spliced (route-fed) broadcast follows the logical tracks'
1116 /// consumers, which is what flips a relay's advertised cost.
1117 #[tokio::test]
1118 async fn demand_spliced() {
1119 tokio::time::pause();
1120
1121 let producer = Producer::new_spliced(Info::new());
1122 let consumer = producer.consume();
1123 let demand = producer.demand();
1124
1125 assert!(!demand.is_used());
1126 let track = consumer.track("video").unwrap();
1127 assert!(demand.is_used());
1128
1129 // Dropping the only consumer wakes a parked `unused`, even though the
1130 // logical track itself stays cached in the broadcast.
1131 let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(track) });
1132 unused.unwrap();
1133 assert!(!demand.is_used());
1134
1135 // A repeat consumer for the cached track counts again.
1136 let _track = consumer.track("video").unwrap();
1137 assert!(demand.is_used());
1138 }
1139
1140 /// Subscribe and assert the result hasn't resolved yet (it stays pending until
1141 /// a publisher accepts). Returns the pending subscription to resolve after accepting.
1142 macro_rules! subscribe_pending {
1143 ($consumer:expr, $name:expr) => {{
1144 let pending = $consumer.track($name).unwrap().subscribe(None);
1145 assert!(
1146 pending.poll_ok(&kio::Waiter::noop()).is_pending(),
1147 "subscribe should stay pending until the request is accepted"
1148 );
1149 pending
1150 }};
1151 }
1152
1153 #[tokio::test]
1154 async fn insert() {
1155 let mut producer = Info::new().produce();
1156
1157 // Create the track before any consumer exists.
1158 let mut track1 = producer.assert_create_track("track1", None);
1159 track1.append_group().unwrap();
1160
1161 let consumer = producer.consume();
1162
1163 // The track already exists, so subscribe resolves immediately.
1164 let mut track1_sub = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1165 track1_sub.assert_group();
1166
1167 let mut track2 = producer.assert_create_track("track2", None);
1168
1169 let consumer2 = producer.consume();
1170 let mut track2_consumer = consumer2.track("track2").unwrap().subscribe(None).await.unwrap();
1171 track2_consumer.assert_no_group();
1172
1173 track2.append_group().unwrap();
1174
1175 track2_consumer.assert_group();
1176 }
1177
1178 #[tokio::test]
1179 async fn closed() {
1180 let mut producer = Info::new().produce();
1181 let dynamic = producer.dynamic();
1182
1183 let consumer = producer.consume();
1184 consumer.assert_not_closed();
1185
1186 // Create a new track and insert it into the broadcast (resolves immediately).
1187 let track1 = producer.assert_create_track("track1", None);
1188 let mut track1c = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1189
1190 // A track nobody publishes stays pending until accepted.
1191 let track2_fut = subscribe_pending!(consumer, "track2");
1192
1193 // Dropping the last dynamic handler rejects pending requests, but must NOT
1194 // cascade to externally-owned tracks.
1195 drop(dynamic);
1196
1197 // track2 was a pending dynamic request, so its subscribe surfaces the rejection.
1198 assert!(track2_fut.await.is_err());
1199
1200 // track1's producer is held outside the broadcast, so it survives.
1201 assert!(!track1.is_closed());
1202 track1c.assert_not_closed();
1203 }
1204
1205 /// `closed()` reports the cause: the abort error, or `Dropped` for a finish or
1206 /// a dropped producer, with `is_finished` telling the latter two apart.
1207 #[tokio::test]
1208 async fn closed_cause() {
1209 // Abort: the error comes through, and it isn't a finish.
1210 let producer = Info::new().produce();
1211 let consumer = producer.consume();
1212 producer.abort(Error::Timeout).unwrap();
1213 assert!(matches!(consumer.closed().await, Error::Timeout));
1214 assert!(!consumer.is_finished());
1215
1216 // Finish: a deliberate clean end.
1217 let mut producer = Info::new().produce();
1218 let consumer = producer.consume();
1219 producer.finish();
1220 assert!(matches!(consumer.closed().await, Error::Dropped));
1221 assert!(consumer.is_finished());
1222
1223 // Plain drop: neither aborted nor finished.
1224 let producer = Info::new().produce();
1225 let consumer = producer.consume();
1226 // Deliberate for the test: exercises the accidental-drop path (warns).
1227 drop(producer);
1228 assert!(matches!(consumer.closed().await, Error::Dropped));
1229 assert!(!consumer.is_finished());
1230 }
1231
1232 #[tokio::test]
1233 async fn requests() {
1234 let mut producer = Info::new().produce().dynamic();
1235
1236 let consumer = producer.consume();
1237 let consumer2 = consumer.clone();
1238
1239 // Two subscribers to the same name coalesce into one request.
1240 let track1_fut = subscribe_pending!(consumer, "track1");
1241 let track2_fut = subscribe_pending!(consumer2, "track1");
1242
1243 // There should be exactly one request to serve.
1244 let request = producer.assert_request();
1245 producer.assert_no_request();
1246 assert_eq!(request.name(), "track1");
1247
1248 // Accept it, which resolves both waiting subscribers.
1249 let mut track3 = request.accept(None);
1250 let mut track1 = track1_fut.await.unwrap();
1251 let mut track2 = track2_fut.await.unwrap();
1252
1253 track1.assert_not_closed();
1254 track1.assert_is_clone(&track2);
1255 track3.subscribe(None).assert_is_clone(&track1);
1256
1257 // Append a group and make sure they all get it.
1258 track3.append_group().unwrap();
1259 track1.assert_group();
1260 track2.assert_group();
1261
1262 // A pending request is cancelled when the dynamic producer is dropped.
1263 let track4_fut = subscribe_pending!(consumer, "track2");
1264 drop(producer);
1265 assert!(track4_fut.await.is_err());
1266
1267 // With no dynamic producer left, requesting the handle fails outright.
1268 let track5 = consumer2.track("track3");
1269 assert!(track5.is_err(), "should have errored");
1270 }
1271
1272 #[tokio::test]
1273 async fn stale_producer() {
1274 let mut broadcast = Info::new().produce().dynamic();
1275 let consumer = broadcast.consume();
1276
1277 // Subscribe to a track and serve it.
1278 let track1_fut = subscribe_pending!(consumer, "track1");
1279 let mut producer1 = broadcast.assert_request().accept(None);
1280 let mut track1 = track1_fut.await.unwrap();
1281
1282 // Close the producer (simulating publisher disconnect).
1283 producer1.append_group().unwrap();
1284 producer1.finish().unwrap();
1285 drop(producer1);
1286
1287 // The consumer should see the track as closed.
1288 track1.assert_closed();
1289
1290 // Subscribe again to the same track: should get a NEW producer, not the stale one.
1291 let track2_fut = subscribe_pending!(consumer, "track1");
1292 let mut producer2 = broadcast.assert_request().accept(None);
1293 let mut track2 = track2_fut.await.unwrap();
1294 track2.assert_not_closed();
1295 track2.assert_not_clone(&track1);
1296
1297 // The new consumer should receive the new group.
1298 producer2.append_group().unwrap();
1299 track2.assert_group();
1300 }
1301
1302 #[tokio::test(start_paused = true)]
1303 async fn requested_unused() {
1304 let mut broadcast = Info::new().produce().dynamic();
1305 let bc = broadcast.consume();
1306
1307 // Subscribe to a track that doesn't exist yet, then serve it.
1308 let c1_fut = subscribe_pending!(bc, "unknown_track");
1309 let producer1 = broadcast.assert_request().accept(None);
1310 let consumer1 = c1_fut.await.unwrap();
1311
1312 // The producer should NOT be unused yet because there's a consumer.
1313 assert!(
1314 producer1.unused().now_or_never().is_none(),
1315 "track producer should be used"
1316 );
1317
1318 // A second subscriber reuses the live producer (fast path / dedup).
1319 let consumer2 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1320 consumer2.assert_is_clone(&consumer1);
1321
1322 drop(consumer1);
1323 assert!(
1324 producer1.unused().now_or_never().is_none(),
1325 "track producer should be used"
1326 );
1327
1328 drop(consumer2);
1329 assert!(
1330 producer1.unused().now_or_never().is_some(),
1331 "track producer should be unused after all consumers are dropped"
1332 );
1333
1334 // While the producer is still alive, re-subscribing to the same name reuses
1335 // it (no new request). This is what lets the relay linger upstream
1336 // subscriptions across transient consumer churn.
1337 let consumer3 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1338 consumer3.assert_is_clone(&producer1.subscribe(None));
1339 broadcast.assert_no_request();
1340 drop(consumer3);
1341
1342 // Aborting the producer closes its lookup entry; the next subscribe sees the
1343 // stale weak, evicts it, and creates a fresh request.
1344 producer1.abort(Error::Cancel).unwrap();
1345
1346 let c4_fut = subscribe_pending!(bc, "unknown_track");
1347 let producer2 = broadcast.assert_request().accept(None);
1348 let consumer4 = c4_fut.await.unwrap();
1349 drop(consumer4);
1350 assert!(
1351 producer2.unused().now_or_never().is_some(),
1352 "new track producer should be unused after its consumer is dropped"
1353 );
1354 }
1355
1356 // Cloning a `Consumer` resets its route cursor: a clone that inherited the
1357 // original's `route_seen` would skip the initial-value delivery that
1358 // `route_changed` promises.
1359 #[tokio::test]
1360 async fn route_clone_observes_current_route() {
1361 let mut producer = Info::new().produce();
1362 let mut consumer = producer.consume();
1363
1364 // Drain the initial route, then a change.
1365 consumer.route_changed().await.unwrap();
1366 let route = Route::new().with_cost(7);
1367 producer.set_route(route.clone()).unwrap();
1368 assert_eq!(consumer.route_changed().await.unwrap(), route);
1369
1370 // The original is fully drained: no update pending.
1371 assert!(consumer.route_changed().now_or_never().is_none());
1372
1373 // A clone starts fresh, yielding the current route immediately.
1374 let mut clone = consumer.clone();
1375 let seen = clone
1376 .route_changed()
1377 .now_or_never()
1378 .expect("clone should observe the current route immediately")
1379 .unwrap();
1380 assert_eq!(seen, route);
1381 }
1382
1383 // Cloning a `Dynamic` and dropping the clone must not flip the handler
1384 // count to zero. The relay's lite subscriber clones the
1385 // dynamic per spawned subscribe; if Clone skipped the increment, the
1386 // first finished subscribe would tear down the broadcast and any
1387 // follow-up `track` would return `NotFound`.
1388 #[tokio::test]
1389 async fn dynamic_clone_keeps_alive() {
1390 let broadcast = Info::new().produce().dynamic();
1391 let consumer = broadcast.consume();
1392
1393 let clone = broadcast.clone();
1394 drop(clone);
1395
1396 // Original handle is still live, so the request registers (stays pending)
1397 // instead of failing with NotFound.
1398 let _fut = subscribe_pending!(consumer, "track1");
1399 }
1400}