diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index a956c96..f6dcee7 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -300,7 +300,8 @@ There MAY be multiple Announce Streams, potentially containing overlapping prefi ### Subscribe A subscriber opens Subscribe Streams to request a Track. -The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE messages. +The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE and SUBSCRIBE_STATS messages. +The opening SUBSCRIBE is identified by the stream type (0x2) and carries no message Type; every subsequent message from the subscriber begins with a Type that distinguishes SUBSCRIBE_UPDATE from SUBSCRIBE_STATS, mirroring how the publisher's messages on this stream are typed. When a start group can be resolved, the publisher replies with a SUBSCRIBE_OK message (confirming the subscription and resolving its start group), followed by any number of SUBSCRIBE_END and SUBSCRIBE_DROP messages. When the accepted track has already ended with no matching groups there is no start group to resolve, so the publisher sends SUBSCRIBE_END with no preceding SUBSCRIBE_OK. A rejection is a stream reset: if the publisher cannot serve the subscription — the track does not exist, or it otherwise refuses — it MUST reset the stream rather than leave it pending, and SHOULD do so promptly (within roughly a round trip) so the subscriber is not left waiting. @@ -775,6 +776,7 @@ The start and end group can be changed in either direction (growing or shrinking ~~~ SUBSCRIBE_UPDATE Message { + Type (i) = 0x0 Message Length (i) Subscriber Priority (8) Subscriber Ordered (8) @@ -784,7 +786,40 @@ SUBSCRIBE_UPDATE Message { } ~~~ -See [SUBSCRIBE](#subscribe) for information about each field. +**Type**: +Set to 0x0 to indicate a SUBSCRIBE_UPDATE message. + +See [SUBSCRIBE](#subscribe) for information about each remaining field. + + +## SUBSCRIBE_STATS +A subscriber sends a SUBSCRIBE_STATS message to report telemetry about a subscription. +Unlike SUBSCRIBE_UPDATE, it does not modify the subscription and the publisher only observes it; it is kept separate so that refreshing telemetry does not re-echo the subscription's delivery parameters on every change. +A subscriber MAY send multiple SUBSCRIBE_STATS messages over the life of the subscription to refresh the values. + +~~~ +SUBSCRIBE_STATS Message { + Type (i) = 0x1 + Message Length (i) + Subscriber Count (i) +} +~~~ + +**Type**: +Set to 0x1 to indicate a SUBSCRIBE_STATS message. + +**Subscriber Count**: +The number of subscribers this subscription represents, encoded as the count minus one: the subscription is the implicit `1`, so a leaf subscriber sends `0`. +This is a subscriber-side value that fans *in* at a relay: a relay merging multiple downstream subscriptions into one upstream subscription sets this to the **sum** of their counts, so the value telescopes up the fan-out tree. +A publisher therefore learns its total number of downstream subscribers across any number of relay hops by reading the count on its single upstream subscription, without any per-hop coordination. +Until a SUBSCRIBE_STATS is received, the count is `1`; a leaf subscriber that represents only itself need not send the message. +A relay holding a subscription open with no live downstream subscribers (e.g. briefly retaining it for reuse) MUST still report at least `1`, since the held subscription cannot represent fewer subscribers than itself. +The count is advisory: a subscriber MAY misreport it, and a relay MUST NOT use it for delivery decisions. + +A relay SHOULD rate-limit SUBSCRIBE_STATS messages (for example, coalescing changes within a short window of roughly a second), so that rapid subscriber churn does not flood the upstream with control messages. +Because the count is the latest aggregate rather than a delta, a change that reverts within the window requires no message at all. + +Future revisions MAY append additional stats to this message; the `Message Length` bounds the message so a receiver can stop after the fields it understands. ## TRACK @@ -1084,6 +1119,7 @@ A generic library or relay MUST NOT inspect or modify the decompressed contents # Appendix A: Changelog ## moq-lite-05 +- Added a SUBSCRIBE_STATS message carrying a telescoping `Subscriber Count` (count of downstream subscribers, encoded as the count minus one so a leaf sends `0`); a relay sums the counts of the downstream subscriptions it merges, so a publisher reads its total audience across any number of hops from its single upstream subscription. It is kept separate from SUBSCRIBE_UPDATE so that refreshing telemetry does not re-echo the subscription's delivery parameters; relays SHOULD rate-limit it to absorb subscriber churn. This also introduced a `Type` tag on the subscriber's post-SUBSCRIBE messages (`0x0` SUBSCRIBE_UPDATE, `0x1` SUBSCRIBE_STATS) to distinguish them, mirroring the publisher's typed responses. - Added a SETUP message, sent once on a unidirectional Setup Stream (0x1) at the start of the session and FIN'd immediately. It carries a list of Setup Parameters for negotiating optional capabilities and extensions per-hop, replacing the prior stream-probing approach (version is still negotiated via ALPN, not SETUP). Endpoints keep exchanging non-Setup streams without waiting for SETUP, buffering only a stream whose encoding a negotiated extension would change; unknown stream types are still reset as a fallback. - Added a SETUP `Probe` parameter advertising the publisher's capability level: `None`, `Report` (measure and report the estimated bitrate), or `Increase` (additionally pad to probe for bandwidth above the current sending rate). The levels are nested since probing without measuring is meaningless. A subscriber must not rely on a level the publisher did not advertise. - Added `Frame Start` to FETCH so a subscriber can begin partway through a group instead of always at frame `0`, allowing resumption of a partially-received group. diff --git a/draft-lcurley-moq-subscribe-stats.md b/draft-lcurley-moq-subscribe-stats.md new file mode 100644 index 0000000..8120eae --- /dev/null +++ b/draft-lcurley-moq-subscribe-stats.md @@ -0,0 +1,211 @@ +--- +title: "MoQ Subscribe Stats Extension" +abbrev: "moq-subscribe-stats" +category: info + +docname: draft-lcurley-moq-subscribe-stats-latest +submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF" +number: +date: +v: 3 +area: wit +workgroup: moq + +author: + - + fullname: Luke Curley + email: kixelated@gmail.com + +normative: + moqt: I-D.ietf-moq-transport + +informative: + +--- abstract + +This document defines a SUBSCRIBE_STATS message for MoQ Transport {{moqt}}: fire-and-forget telemetry that a subscriber reports about a subscription without modifying it. +It carries one or more stats that aggregate cheaply up the relay fan-out tree. +This document defines a single stat, Subscriber Count, which telescopes so a publisher can learn its total downstream audience across any number of relay hops by reading one value on its upstream subscription. + +--- middle + +# Conventions and Definitions +{::boilerplate bcp14-tagged} + + +# Introduction +A publisher in {{moqt}} often wants to know how many subscribers are receiving a Track. +This is straightforward when a subscriber connects directly, but {{moqt}} is designed around relays: a relay aggregates many downstream subscriptions for the same Track into a single upstream subscription toward the origin (its "fan-out" tree). +The origin sees one upstream subscription per relay, not the individual subscribers behind it, so it cannot count its true audience without out-of-band coordination. + +This document defines a SUBSCRIBE_STATS message that reports telemetry about a subscription. +A stat is useful here only if it aggregates cheaply up the fan-out tree: a relay merges the stats of the downstream subscriptions it serves into one value on its single upstream subscription. +This document defines a single such stat, **Subscriber Count**, which a relay reports as the **sum** of its downstream counts. +Because the count is a sum reduced up the existing subscription tree, it telescopes for free: at the origin, the count on each upstream subscription is the total number of subscribers reachable through that relay, transitively, across any number of hops. + +Stats change as subscribers join and leave, so SUBSCRIBE_STATS is sent repeatedly over the life of a subscription. +This is pure telemetry: it never modifies the subscription, and the publisher only observes it. +For that reason it is a dedicated, fire-and-forget message rather than a parameter on REQUEST_UPDATE ({{moqt}} Section 10.9). +A REQUEST_UPDATE consumes a Request ID ({{moqt}} Section 10.1) and obliges the receiver to answer with a REQUEST_OK or REQUEST_ERROR ({{moqt}} Section 10.9) — a request/response transaction whose purpose is to *modify* the subscription, which is a poor fit for a value pushed once per second that changes nothing. +SUBSCRIBE_STATS instead rides the subscription's existing request stream, consumes no Request ID, and elicits no response. + + +# Setup Negotiation +The Subscribe Stats extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 9.4. + +Both endpoints indicate support by including the following Setup Option: + +~~~ +SUBSCRIBE_STATS Setup Option { + Option Key (vi64) = 0xC0117 + Option Value Length (vi64) = 0 +} +~~~ + +The extension is available on a hop only if both endpoints on that hop included this option. +The extension is negotiated independently on each hop: a relay MAY support it upstream but not downstream, or vice versa. + +Negotiation is mandatory before the message is sent. +{{moqt}} (Section 10) requires an endpoint that receives an unknown control message type to close the session, so — unlike an optional parameter, which can be ignored — a SUBSCRIBE_STATS message cannot be sent speculatively. +An endpoint MUST NOT send SUBSCRIBE_STATS on a hop that did not negotiate this extension. + + +# SUBSCRIBE_STATS Message +This document defines a new control message, sent on a subscription's request stream ({{moqt}} Section 3.3) by the endpoint that opened it (the subscriber, which for an upstream subscription is the relay). + +~~~ +SUBSCRIBE_STATS Message { + Type (vi64) = 0xC0117 + Length (16) + Number of Stats (vi64) + Stats (..) ... +} +~~~ + +The message MUST NOT be the first message on the request stream; it follows the SUBSCRIBE ({{moqt}} Section 10.7) that opened the stream. +It consumes no Request ID ({{moqt}} Section 10.1), and the receiver MUST NOT respond to it. +A subscriber MAY send it any number of times over the life of the subscription to refresh the stats. + +**Number of Stats**: +The number of stats that follow. + +**Stats**: +Each stat is encoded as a Message Parameter ({{moqt}} Section 10.2): a `Type` (drawn from the same "Message Parameters" registry) and a value, where an even `Type` carries a single varint and an odd `Type` is length-prefixed ({{moqt}} Section 1.4.3). +A receiver MUST ignore a stat whose `Type` it does not recognize, so future stats can be added without a new message type or a wire break. +This document defines one stat, [Subscriber Count](#subscriber-count). + + +# Subscriber Count {#subscriber-count} + +~~~ +SUBSCRIBER_COUNT Stat { + Type (vi64) = 0xC0116 + Value (vi64) ; Subscriber Count - 1 +} +~~~ + +The `Type` `0xC0116` is even, so per {{moqt}} Section 1.4.3 the value is a single varint with no length prefix. + +**Value**: +The number of subscribers this subscription represents, including the subscriber itself, encoded as the count minus one. +The subscription is the implicit `1`, so the wire value is `Subscriber Count - 1`: a leaf encodes `0`, and a relay encodes its summed total minus one. + +Encoding `count - 1` rather than the count itself makes a count of `0` impossible to represent on the wire: the minimum encodable value is `0`, which decodes to a count of `1`. +A subscription can never report fewer subscribers than itself. + +Until a SUBSCRIBE_STATS carrying this stat is received for a subscription, its count is `1`. +A leaf subscriber that represents only itself therefore need not send it at all. + + +# Semantics +The Subscriber Count is a reduction up the subscription tree. + +A **leaf subscriber** (one that is not a relay) represents itself: a count of `1`. +It need not report the stat to mean this. + +A **relay** that aggregates one or more downstream subscriptions for a Track into a single upstream subscription sets the count it reports on that upstream subscription to the **sum** of the counts of the downstream subscriptions, treating a downstream subscription that has not reported as `1`. +When a downstream count changes, or a downstream subscription is added or removed, the relay recomputes the sum and, if it changed, sends a SUBSCRIBE_STATS message upstream with the new total. + +A relay MUST report at least `1` for any upstream subscription it is holding open, even if it currently has no live downstream subscribers (for example, a subscription briefly retained for reuse). +The `count - 1` encoding enforces this floor automatically: a sum of `0` still encodes a wire value of `0`, which decodes upstream as `1`. +A held subscription cannot represent fewer subscribers than itself. + +Because each relay reports the sum of its subtree, the value telescopes: at the origin, the count on a given upstream subscription is the total number of leaf subscribers reachable through that subscription, across any number of relay hops. +A publisher reads its total audience for a Track as the sum of the counts of the subscriptions it is serving. + +SUBSCRIBE_STATS alters no delivery behavior. +It MUST NOT influence prioritization, caching, congestion response, or any other distribution decision; it is informational telemetry carried alongside the subscription. + + +# Rate Limiting +Subscriber churn can change a stat rapidly, and at a busy relay each change would otherwise produce an upstream SUBSCRIBE_STATS message. + +A relay SHOULD rate-limit SUBSCRIBE_STATS messages per subscription, coalescing changes that occur within a short window (on the order of a second) and then sending the latest values. +Because each stat carries its current value rather than a delta, a change that reverts within the window — a subscriber that joins and leaves, or leaves and returns — requires no upstream message at all. + +Because the message is independent of REQUEST_UPDATE, this rate limiting never delays a genuine subscription change: delivery-affecting updates are forwarded according to {{moqt}} without regard to the stats window. + + +# Security Considerations +**Audience disclosure.** +The Subscriber Count discloses aggregate viewership to the publisher and to every relay on the path toward it. +For some applications the size of an audience is sensitive (for example, it can reveal the popularity or reach of content, or that an audience has dropped to zero). +Because the extension is negotiated per hop, an endpoint that considers a stat sensitive simply does not advertise the SUBSCRIBE_STATS Setup Option, and no stats are exchanged on that hop. + +**Untrusted values.** +Stats are supplied by the subscriber side and aggregated by intermediaries, none of which the publisher can fully trust. +A malicious or buggy subscriber can report an inflated or deflated count, and a malicious relay can report any sum regardless of its actual downstream subscriptions. +Stats are therefore advisory: an endpoint MUST NOT use one for any security-sensitive purpose — such as billing, admission control, rate limiting, or capacity planning that affects other subscribers — without independent verification. +A relay aggregates the values it receives but cannot attest to the honesty of its downstream peers. + +**Churn amplification.** +A subscriber that rapidly joins and leaves could attempt to amplify control traffic toward the origin by forcing repeated stat updates. +The rate-limiting in [Rate Limiting](#rate-limiting) bounds this: the upstream SUBSCRIBE_STATS rate per subscription is capped regardless of downstream churn, and reverts within the window are suppressed entirely. +Because the message consumes no Request ID and elicits no response, this churn cannot exhaust an identifier space or force the origin into matching replies. + +This extension introduces no other security considerations beyond those described in {{moqt}}. + + +# IANA Considerations + +This document requests the following registrations. + +## MOQT Setup Options + +This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required. +moq-transport defines no private-use range for Setup Options; extensions request a (provisional) codepoint. +A high, distinctive value is chosen to avoid the low ranges reserved by {{moqt}} and to minimize collisions with provisional registrations by other extensions; it also avoids the greasing pattern (`0x7f * N + 0x9D`). + +| Value | Name | Reference | +|:------|:-----|:----------| +| 0xC0117 | SUBSCRIBE_STATS | This Document | + +## MOQT Message Types + +This document registers a control message type. +{{moqt}} does not yet establish an IANA registry for message types, so this is a provisional codepoint pending such a registry; the value is chosen to be high and distinctive to avoid the low ranges {{moqt}} assigns and to minimize collisions with provisional registrations by other extensions, and it avoids the greasing pattern (`0x7f * N + 0x9D`). +This is the same value as the SUBSCRIBE_STATS Setup Option above; Setup Options and message types are independent namespaces, so the shared value is unambiguous. + +The Stream column has the meaning defined by {{moqt}} Section 10: "Request" indicates the message is carried on a bidirectional request stream. The message is not marked "First": it never opens a request stream. + +| Value | Name | Stream | Reference | +|:------|:-----|:-------|:----------| +| 0xC0117 | SUBSCRIBE_STATS | Request | This Document | + +## MOQT Message Parameters + +This document requests a registration in the "Message Parameters" registry ({{moqt}} Section 15.7), used here to carry stats inside SUBSCRIBE_STATS. +The value is **even** so that, per the Key-Value-Pair encoding ({{moqt}} Section 1.4.3), the count is carried as a single varint with no length prefix. +A high, distinctive value is chosen to avoid the low ranges reserved by {{moqt}} and to minimize collisions with provisional registrations by other extensions; it also avoids the greasing pattern (`0x7f * N + 0x9D`). + +| Value | Name | Reference | +|:------|:-----|:----------| +| 0xC0116 | SUBSCRIBER_COUNT | This Document | + + +--- back + +# Acknowledgments +{:numbered="false"} + +This document was drafted with the assistance of Claude, an AI assistant by Anthropic.