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