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