moq_net/model/subscription.rs
1use std::{
2 ops::{Bound, RangeBounds, RangeFull, RangeTo, RangeToInclusive},
3 task::Poll,
4 time::Duration,
5};
6
7/// Subscriber-side preferences for receiving a track.
8///
9/// Each subscriber holds its own [`Subscription`]; the publisher observes an
10/// aggregate across all live subscribers via [`crate::track::Producer::subscription`].
11/// A subscriber can change its preferences after the fact with
12/// [`crate::track::Subscriber::update`].
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct Subscription {
16 /// Delivery priority. Higher values preempt lower ones when bandwidth is constrained.
17 pub priority: u8,
18 /// How old a group may get before this subscriber gives up on it.
19 ///
20 /// [`Duration::ZERO`] (the default) skips immediately: group 8 arriving means group 7
21 /// is abandoned. A larger budget tolerates that much reordering before giving up.
22 /// This never *adds* delay, since the bound is only reached once newer data is
23 /// already that far ahead.
24 ///
25 /// This is the `Subscriber Max Age` on the wire, and it is stored here verbatim so
26 /// what was asked for stays readable. Encoded as milliseconds in a QUIC varint, so
27 /// a duration of `2^62` milliseconds or more cannot be put on the wire. Clamped to
28 /// the publisher's [`Info::max_age`](crate::track::Info::max_age), since waiting for
29 /// a group longer than it is kept around cannot produce it.
30 ///
31 /// # Where it is enforced
32 ///
33 /// At both ends, and neither alone is enough. The publisher skips a group that has
34 /// aged out instead of putting it on the wire, which bounds a backlog before it
35 /// costs bandwidth. But what reaches the publisher is the aggregate across every
36 /// subscriber, resolved in favor of the most tolerant one, so that gate is only ever
37 /// as tight as the most patient viewer. The subscriber applies the same budget again
38 /// as it reads, where its own is the only one in play.
39 ///
40 /// This bounds a *subscription*.
41 /// [`track::Consumer::fetch_group`](crate::track::Consumer::fetch_group) is exempt:
42 /// it names one old group explicitly, so there is no live edge to be late against.
43 ///
44 /// # How age is measured
45 ///
46 /// In presentation time only. A group is measured by its *reach*, where its immediate
47 /// successor begins, against the newest frame of the latest group: it cannot present
48 /// past its successor, so once everything it could still hold falls outside the budget
49 /// it is provably useless. The candidate needs no timestamp of its own, so an empty or
50 /// stalled group is bounded by its stamped successor the same way. Wall-clock
51 /// reclamation of idle content is the cache's own policy, not this budget's.
52 ///
53 /// Protocols whose wire can't carry a timestamp (pre-Lite05 moq-lite, moq-transport
54 /// without the Timestamp property) have their frames stamped on receipt, which makes
55 /// the measure burst-blind on the receiving side: thirty seconds of backlog delivered
56 /// in three reads as three. The publisher's copy is stamped as it produces, so the
57 /// gate there still holds; it is just the coarser of the two.
58 pub max_age: Duration,
59 /// The lowest [`Position`] the publisher may deliver, or `None` for no floor.
60 ///
61 /// A floor, not a request: only [`Self::max_age`] asks for data, and the floor bounds
62 /// how far back it may reach. `None` and a floor of group 0 mean the same thing, since
63 /// nothing sits below group 0. Delivery starts at the oldest group at or above the
64 /// floor that the budget still considers fresh, so a floor above the live edge simply
65 /// waits there (a resumed subscription naming where it left off).
66 ///
67 /// Aggregated across every live subscriber (the loosest floor wins, and any subscriber
68 /// without one clears it), so it says what the publisher sends, not what any one
69 /// subscriber sees. [`crate::track::Subscriber::set_groups`] is the local read cursor;
70 /// setting one does not imply the other. See [Local cursor vs wire
71 /// preference](crate::track::Subscriber#local-cursor-vs-wire-preference).
72 pub start: Option<Position>,
73 /// First [`Position`] the publisher should *not* deliver, or `None` for no end.
74 ///
75 /// Exclusive, like the end of a [`std::ops::Range`], which is what lets one field
76 /// carry both "through the end of group 5" ([`Position::after_group(5)`](Position::after_group))
77 /// and "up to frame 2 of group 5" ([`Position::after(5, 2)`](Position::after)). An
78 /// inclusive end cannot express the first without a sentinel frame, and the ordering
79 /// falls out for free: group 6's head sorts above any frame of group 5, so a
80 /// whole-group subscriber correctly absorbs a frame-capped one in the aggregate.
81 ///
82 /// The wire agrees: `Group End` and `Frame End` are both encoded as `absolute + 1`.
83 ///
84 /// A request, aggregated across every live subscriber (any unbounded subscriber makes
85 /// the aggregate unbounded). [`crate::track::Subscriber::set_groups`] is the local read
86 /// cursor; [`Position::group_end`] translates this field into its bound. Setting one
87 /// does not imply the other.
88 pub end: Option<Position>,
89}
90
91impl Default for Subscription {
92 fn default() -> Self {
93 Self {
94 priority: 0,
95 max_age: Duration::ZERO,
96 start: None,
97 end: None,
98 }
99 }
100}
101
102impl Subscription {
103 /// Set the delivery priority, returning `self` for chaining.
104 pub fn with_priority(mut self, priority: u8) -> Self {
105 self.priority = priority;
106 self
107 }
108
109 /// Set how old a group may get before it is skipped, returning `self` for chaining.
110 pub fn with_max_age(mut self, max_age: Duration) -> Self {
111 self.max_age = max_age;
112 self
113 }
114
115 /// Floor delivery at `start`, or leave it unfloored when `None`. Returns `self` for
116 /// chaining.
117 ///
118 /// A floor bounds how far back [`Self::max_age`] may reach; it does not request data
119 /// on its own. [`Position::group`] is the whole-group form.
120 pub fn with_start(mut self, start: impl Into<Option<Position>>) -> Self {
121 self.start = start.into();
122 self
123 }
124
125 /// Stop delivery at `end`, or leave the subscription unbounded when `None`. Returns
126 /// `self` for chaining.
127 ///
128 /// Exclusive, matching [`Self::end`], so pass the position *after* the last one you
129 /// want. [`Position::after`] and [`Position::after_group`] name that conversion so no
130 /// call site has to write the `+ 1` itself.
131 pub fn with_end(mut self, end: impl Into<Option<Position>>) -> Self {
132 self.end = end.into();
133 self
134 }
135
136 /// Request the whole groups in `groups`, replacing both [`Self::start`] and
137 /// [`Self::end`]. Returns `self` for chaining.
138 ///
139 /// Any range of group sequences works: `2..=5`, `2..6`, `..6`, `2..`, or `..` to
140 /// clear both bounds. An inclusive end past the last group is unbounded, as
141 /// [`Position::after_group`] spells it.
142 pub fn with_groups(mut self, groups: impl RangeBounds<u64>) -> Self {
143 let unbounded_start = matches!(groups.start_bound(), Bound::Unbounded);
144 let (start, end) = sequence_bounds(groups);
145 self.start = (!unbounded_start).then(|| Position::group(start));
146 self.end = end.map(Position::group);
147 self
148 }
149
150 // Fold this subscription into the running aggregate: Ready with the merged
151 // result when it demands more than `combined`, Pending when it's a subset
152 // (so callers can skip a redundant broadcast of the same aggregate).
153 pub(super) fn poll_combined(&self, combined: &Option<Subscription>) -> Poll<Subscription> {
154 let Some(combined) = combined else {
155 return Poll::Ready(self.clone());
156 };
157
158 let merged = Subscription {
159 priority: self.priority.max(combined.priority),
160 // Sequence-first prioritization is enabled only when every subscriber wants it.
161 max_age: self.max_age.max(combined.max_age),
162 // Bounds fold as whole positions. Two subscribers starting in the same group
163 // are separated only by their frame, so folding group and frame independently
164 // would invent a bound neither asked for.
165 start: min_floored(self.start, combined.start),
166 end: max_unbounded(self.end, combined.end),
167 };
168
169 if &merged != combined {
170 return Poll::Ready(merged);
171 }
172
173 Poll::Pending
174 }
175}
176
177/// A frame-precise point in a track: a group sequence and a frame index within it.
178///
179/// Ordered lexicographically, so comparing positions is the same as comparing groups
180/// and only falling back to frames within one. This is the model's counterpart of the
181/// wire's (`Group`, `Frame`) pairs on SUBSCRIBE and FETCH.
182///
183/// Pairing the two is the point: a frame index counts from the start of a group, so it
184/// means nothing on its own. Carrying them together makes "frame 5 of nothing"
185/// unrepresentable rather than merely undefined.
186#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
187pub struct Position {
188 /// The group sequence.
189 pub group: u64,
190 /// The frame index within the group, numbered from 0 in write order.
191 pub frame: u64,
192}
193
194impl Position {
195 /// The first frame of `group`.
196 ///
197 /// As an exclusive end this means "everything before `group`"; as a start it means
198 /// "`group` from the beginning".
199 pub fn group(group: u64) -> Self {
200 Self { group, frame: 0 }
201 }
202
203 /// The position just past `frame` of `group`: the exclusive end that includes it.
204 ///
205 /// `None` when there is no such position, i.e. the very last frame of the very last
206 /// group. That is past everything, which [`Subscription::end`] spells `None` too, so
207 /// the two meanings line up and `with_end` can take this directly.
208 pub fn after(group: u64, frame: u64) -> Option<Self> {
209 match frame.checked_add(1) {
210 Some(frame) => Some(Self { group, frame }),
211 // Past the last frame of a group is the head of the next one.
212 None => Self::after_group(group),
213 }
214 }
215
216 /// The position just past every frame of `group`: the exclusive end that includes
217 /// the group whole.
218 ///
219 /// `None` past the last group, for the reason given on [`Self::after`].
220 pub fn after_group(group: u64) -> Option<Self> {
221 Some(Self::group(group.checked_add(1)?))
222 }
223
224 /// The bound this exclusive end puts on a group cursor, for
225 /// [`crate::track::Subscriber::set_groups`].
226 ///
227 /// A head-of-group end excludes its group. A mid-group end includes it, so a frame
228 /// cap can apply within that group.
229 pub fn group_end(self) -> Bound<u64> {
230 if self.frame == 0 {
231 Bound::Excluded(self.group)
232 } else {
233 Bound::Included(self.group)
234 }
235 }
236}
237
238/// Where a read cursor stops: a group sequence or frame index it will not deliver.
239///
240/// Built from a range so the call site says whether its bound is delivered: `..5` stops
241/// before 5, `..=5` reads through it, and `..` removes the cap. A [`Bound`] converts
242/// too, for callers holding one (a decoded wire field, or [`Position::group_end`]).
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub(crate) struct Cap(Option<u64>);
245
246impl Cap {
247 /// The first index withheld, or `None` for no cap. Cursors store this form and
248 /// compare with [`before_end`]. An inclusive bound at `u64::MAX` has nothing above
249 /// it, so it is no cap at all.
250 pub(crate) fn exclusive(self) -> Option<u64> {
251 self.0
252 }
253}
254
255impl From<Bound<u64>> for Cap {
256 fn from(bound: Bound<u64>) -> Self {
257 Self(match bound {
258 Bound::Included(index) => index.checked_add(1),
259 Bound::Excluded(index) => Some(index),
260 Bound::Unbounded => None,
261 })
262 }
263}
264
265impl From<RangeTo<u64>> for Cap {
266 fn from(range: RangeTo<u64>) -> Self {
267 Self(Some(range.end))
268 }
269}
270
271impl From<RangeToInclusive<u64>> for Cap {
272 fn from(range: RangeToInclusive<u64>) -> Self {
273 Bound::Included(range.end).into()
274 }
275}
276
277impl From<RangeFull> for Cap {
278 fn from(_: RangeFull) -> Self {
279 Self(None)
280 }
281}
282
283// Normalize discrete ranges once, including the empty range above the last index.
284pub(super) fn sequence_bounds(range: impl RangeBounds<u64>) -> (u64, Option<u64>) {
285 let start = match range.start_bound() {
286 Bound::Included(&start) => start,
287 Bound::Excluded(&start) => match start.checked_add(1) {
288 Some(start) => start,
289 None => return (u64::MAX, Some(u64::MAX)),
290 },
291 Bound::Unbounded => 0,
292 };
293 (start, Cap::from(range.end_bound().cloned()).exclusive())
294}
295
296/// Whether `sequence` is strictly below an exclusive cap. `None` is unbounded.
297pub(super) fn before_end(sequence: u64, end: Option<u64>) -> bool {
298 end.is_none_or(|end| sequence < end)
299}
300
301// Combining two optional bounds comes in two families, and they disagree on what `None`
302// means. `_some` treats it as the neutral element (the other side wins), for intersecting
303// two ranges that each restrict independently. `_floored` / `_unbounded` treat it as
304// absorbing (the result is `None` too), for aggregating across subscribers, where one
305// subscriber asking for everything makes the aggregate everything. Picking the wrong
306// family silently narrows or widens what the publisher sends, so the suffix, not the
307// `min`/`max`, is the part to read.
308
309/// The lower of two optional bounds, `None` neutral. Pairs with [`max_some`].
310pub(super) fn min_some<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
311 match (a, b) {
312 (Some(a), Some(b)) => Some(a.min(b)),
313 (Some(a), None) | (None, Some(a)) => Some(a),
314 (None, None) => None,
315 }
316}
317
318/// The higher of two optional bounds, `None` neutral. Pairs with [`min_some`].
319pub(super) fn max_some<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
320 match (a, b) {
321 (Some(a), Some(b)) => Some(a.max(b)),
322 (Some(a), None) | (None, Some(a)) => Some(a),
323 (None, None) => None,
324 }
325}
326
327/// The lower of two optional floors, `None` absorbing (no floor). The mirror of
328/// [`max_unbounded`]: both bounds only ever *restrict*, so a subscriber without one keeps
329/// the aggregate unrestricted.
330pub(super) fn min_floored<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
331 match (a, b) {
332 (Some(a), Some(b)) => Some(a.min(b)),
333 (None, _) | (_, None) => None,
334 }
335}
336
337/// The higher of two optional bounds, `None` absorbing (unbounded).
338pub(super) fn max_unbounded<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
339 match (a, b) {
340 (Some(a), Some(b)) => Some(a.max(b)),
341 (None, _) | (_, None) => None,
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn combine(subscriptions: &[Subscription]) -> Option<Subscription> {
350 let mut combined = None;
351 for sub in subscriptions {
352 if let Poll::Ready(merged) = sub.poll_combined(&combined) {
353 combined = Some(merged);
354 }
355 }
356 combined
357 }
358
359 /// The exclusive representation runs out at both extremes, and `Option` says so
360 /// rather than saturating into a bound that contradicts the request.
361 /// A group range spells both positions at once, in whichever form the caller has.
362 #[test]
363 fn group_ranges_build_whole_group_positions() {
364 let sub = Subscription::default().with_groups(2..=5);
365 assert_eq!(sub.start, Some(Position::group(2)));
366 assert_eq!(sub.end, Some(Position::group(6)));
367
368 let sub = Subscription::default().with_groups(2..6);
369 assert_eq!(sub.end, Some(Position::group(6)));
370
371 let sub = Subscription::default().with_groups(..6);
372 assert_eq!(sub.start, None);
373 assert_eq!(sub.end, Some(Position::group(6)));
374
375 let sub = Subscription::default().with_groups(2..);
376 assert_eq!(sub.start, Some(Position::group(2)));
377 assert_eq!(sub.end, None);
378
379 // Through the last group is unbounded, as `after_group` spells it.
380 let sub = Subscription::default().with_groups(..=u64::MAX);
381 assert_eq!(sub.end, None);
382
383 let sub = Subscription::default().with_groups(2..=5).with_groups(..);
384 assert_eq!((sub.start, sub.end), (None, None));
385 }
386
387 #[test]
388 fn positions_are_total_at_the_extremes() {
389 // Past the last frame of a group is the head of the next one, not a wider frame
390 // in the same group.
391 assert_eq!(Position::after(5, u64::MAX), Some(Position::group(6)));
392
393 // Past everything has no position. `Subscription::end` spells that `None` too,
394 // so the meanings coincide and the group is included rather than dropped.
395 assert_eq!(Position::after_group(u64::MAX), None);
396 assert_eq!(Position::after(u64::MAX, u64::MAX), None);
397 assert_eq!(
398 Subscription::default().with_end(Position::after_group(u64::MAX)).end,
399 None
400 );
401
402 // A head-of-group end excludes that group; a mid-group end includes it.
403 assert_eq!(Position::group(0).group_end(), Bound::Excluded(0));
404 assert_eq!(Position::after_group(5).unwrap().group_end(), Bound::Excluded(6));
405 assert_eq!(Position::after(5, 2).unwrap().group_end(), Bound::Included(5));
406
407 // A cursor cap is the first index it withholds; an inclusive bound at the last
408 // index withholds nothing.
409 assert_eq!(Cap::from(..0).exclusive(), Some(0));
410 assert_eq!(Cap::from(..=5).exclusive(), Some(6));
411 assert_eq!(Cap::from(..=u64::MAX).exclusive(), None);
412 assert_eq!(Cap::from(..).exclusive(), None);
413 assert_eq!(Cap::from(Bound::Included(5)), Cap::from(..6));
414 assert_eq!(Cap::from(Bound::Unbounded), Cap::from(..));
415 }
416
417 #[test]
418 fn combined_group_start_keeps_the_loosest_floor() {
419 // A floor only restricts, so the lowest one wins across floored subscribers.
420 let catchup = Subscription::default().with_start(Position::group(10));
421 let older_catchup = Subscription::default().with_start(Position::group(5));
422 let combined = combine(&[catchup.clone(), older_catchup]).unwrap();
423 assert_eq!(combined.start, Some(Position::group(5)));
424
425 // A subscriber with no floor at all clears the aggregate: its budget may reach
426 // below any floor the others set.
427 let unfloored = Subscription::default();
428 let combined = combine(&[catchup, unfloored]).unwrap();
429 assert_eq!(combined.start, None);
430 }
431
432 #[test]
433 fn combined_group_end_keeps_live_subscription_unbounded() {
434 // No end at all is unbounded, which absorbs any explicit one.
435 let live = Subscription::default();
436 let bounded = Subscription::default().with_end(Position::after_group(10));
437
438 let combined = combine(&[live, bounded]).unwrap();
439
440 assert_eq!(combined.end, None);
441 }
442
443 #[test]
444 fn combined_start_folds_the_whole_position() {
445 let early_frame = Subscription::default().with_start(Position { group: 5, frame: 2 });
446 let late_frame = Subscription::default().with_start(Position { group: 5, frame: 9 });
447
448 // Same group: the earlier frame wins.
449 let combined = combine(&[late_frame.clone(), early_frame.clone()]).unwrap();
450 assert_eq!(combined.start, Some(Position { group: 5, frame: 2 }));
451
452 // An earlier group wins outright, carrying its own frame rather than the
453 // smallest frame across the two.
454 let earlier_group = Subscription::default().with_start(Position { group: 4, frame: 7 });
455 let combined = combine(&[early_frame, earlier_group]).unwrap();
456 assert_eq!(combined.start, Some(Position { group: 4, frame: 7 }));
457 }
458
459 #[test]
460 fn combined_end_folds_the_whole_position() {
461 let short = Subscription::default().with_end(Position::after(5, 2));
462 let long = Subscription::default().with_end(Position::after(5, 9));
463
464 // Same group: the later frame wins. Ends are exclusive, so an inclusive frame 9
465 // is stored as 10.
466 let combined = combine(&[short.clone(), long.clone()]).unwrap();
467 assert_eq!(combined.end, Some(Position { group: 5, frame: 10 }));
468
469 // The whole group is the head of the next one, which outsorts every frame of
470 // this one, so it absorbs any capped end without a sentinel.
471 let whole = Subscription::default().with_end(Position::after_group(5));
472 let combined = combine(&[long, whole]).unwrap();
473 assert_eq!(combined.end, Some(Position::group(6)));
474
475 // A later group wins outright, carrying its own frame.
476 let later_group = Subscription::default().with_end(Position::after(6, 1));
477 let combined = combine(&[short, later_group]).unwrap();
478 assert_eq!(combined.end, Some(Position { group: 6, frame: 2 }));
479 }
480
481 #[test]
482 fn combined_group_end_uses_latest_bounded_end() {
483 let early = Subscription::default().with_end(Position::after_group(10));
484 let late = Subscription::default().with_end(Position::after_group(20));
485
486 let combined = combine(&[early, late]).unwrap();
487
488 assert_eq!(combined.end, Some(Position::group(21)));
489 }
490}