feat(moq-net): downstream viewer count on subscriptions (lite-05)#1672
Open
kixelated wants to merge 3 commits into
Open
feat(moq-net): downstream viewer count on subscriptions (lite-05)#1672kixelated wants to merge 3 commits into
kixelated wants to merge 3 commits into
Conversation
Carry a per-subscription viewer count that telescopes up the relay fan-out tree, so a publisher can learn its total downstream audience across any number of hops. The count rides on `Subscription` as a `downstream` weight. A relay dedups N downstream subscribers into one upstream subscription, so the producer's aggregate sums every subscriber's weight and the relay forwards that sum upstream. A leaf reports 1; a relay reports its summed total. This telescopes for free: viewer count is a reduction up the existing subscription tree. Wire (lite-05+ only, gated on `has_viewer_count()`): SUBSCRIBE and SUBSCRIBE_UPDATE carry `downstream - 1`. The subscriber itself is the implicit 1, so a leaf encodes 0 (nothing downstream of it) and decode adds the 1 back. A paused relay (0 viewers) saturates to wire 0 and reads back as 1, since a held subscription can't represent fewer than itself. Older drafts omit the field and every subscription weighs 1. The aggregation refactor is the subtle part: the old combine fold doubled as wakeup-registration and close-pruning (a subscriber returning Pending was how its waiter got registered). Summing always "changes" the result, which would break both. So registration and the value merge are now decoupled: each live subscriber is polled with a Pending closure purely to register the waiter and detect closure, then its value is read and merged separately. This also fixes a latent snapshot over-count when a unique-preference subscriber dropped. Tests: wire round-trip across lite-05/lite-04 (leaf -> 0, paused -> 1), model sum + prune-on-drop, and an end-to-end relay test standing up a real moq-relay over WebTransport where two subscribers telescope to a single count of 2 at the publisher (not per-hop) and back to 1 on disconnect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Damp flapping when consumers (and thus the viewer count) churn rapidly. After a relay sends a control message upstream for a subscription, it holds further subscription changes for SUBSCRIBE_THROTTLE (1s), then forwards the latest aggregate. The serve loop suppresses subscription polling during the window and re-polls when its timer fires. Because the aggregate is level-triggered, a change that reverts within the window (e.g. a viewer joins and leaves) produces no upstream message at all. handle_subscription now reports whether it actually sent, so the window only arms on a real send. Adds relay_throttles_subscribe_updates: a second viewer's count bump is held behind the first's window, so the publisher can't observe it near-instantly (lenient lower bound to stay CI-robust). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-count # Conflicts: # rs/moq-net/src/lite/subscriber.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Carry a per-subscription viewer count that telescopes up the relay fan-out tree, so a publisher can learn its total downstream audience across any number of hops.
The count rides on
Subscriptionas adownstreamweight. A relay dedups N downstream subscribers into one upstream subscription, so the producer's aggregate sums every subscriber's weight and the relay forwards that sum upstream. A leaf reports 1; a relay reports its summed total. Viewer count is just a reduction up the existing subscription tree, so it telescopes for free.Read the total on the publisher side with
producer.subscription().map(|s| s.downstream).Wire format (lite-05+ only)
Gated on a new
Version::has_viewer_count()(lite-05-wip+).SUBSCRIBEandSUBSCRIBE_UPDATEcarrydownstream - 1:1, so a leaf encodes0(nothing downstream of it); decode adds the1back. This is what makes the field name "downstream" accurate.0and reads back as1upstream — a held subscription can't represent fewer viewers than itself.1(no telescoping, but each hop counts its direct subscribers). No interop break: lite-05-wip isn't ALPN-advertised and JS doesn't negotiate it yet.Throttle (anti-flap)
The relay spaces upstream control messages for a subscription by at least 1s (
SUBSCRIBE_THROTTLE). After sending a SUBSCRIBE/SUBSCRIBE_UPDATE it holds further subscription changes until the window elapses, then forwards the latest aggregate. The serve loop suppresses subscription polling during the window and re-polls when its timer fires.Because the aggregate is level-triggered, a change that reverts within the window (a viewer joins and leaves, or leaves and returns) produces no upstream message at all.
handle_subscriptionnow reports whether it actually sent, so the window only arms on a real send.The aggregation refactor (the subtle part)
The existing combine fold doubled as wakeup-registration and close-pruning: a subscriber returning
Poll::Pending(its prefs already subsumed) was how its channel waiter got registered and how dropped consumers got pruned. Summing makes the merge always "change" the result, which silently broke both.So registration and the value merge are now decoupled: each live subscriber is polled with a
Pendingclosure purely to register the waiter and detect closure, then its value is read and merged separately (Subscription::merge). Bonus: this also fixes a latent snapshot over-count when a unique-preference subscriber dropped.Tests
lite/subscribe.rs): round-trip across lite-05/lite-04, leaf → wire0, paused0→ reads back1.model/subscription.rs,model/track.rs): sum across subscribers, prune-on-drop, default 1.moq-relay/tests/smoke.rs): stands up an actualmoq-relay(Cluster+Connection) over WebTransport pinned to lite-05-wip.relay_telescopes_downstream_viewer_count: two subscribers telescope to a single count of 2 at the publisher (not per-hop), then back to 1 on disconnect.relay_throttles_subscribe_updates: a second viewer's bump is held behind the first's throttle window, so the publisher can't observe it near-instantly.All 376 moq-net lib tests + the relay smoke suite pass (stable across repeated runs); clean clippy/fmt via the pinned nix toolchain.
Test plan
cargo test -p moq-net --libcargo test -p moq-relay --test smoke(telescoping + throttle, stable across repeated runs)cargo clippy+cargo fmtvia nixhang,libmoq,moq-relay,moq-cli,moq-ffi)Follow-ups (not in this PR)
js/netwire +Subscription, anddoc/concept. No wire breakage today (lite-05-wip-gated), so deferred.Branch
Targets
devper the branch-targeting policy (lite wire-protocol change underrs/moq-net).(Written by Claude)