Skip to content

feat!: replace moq --hop with --epoch and stop stamping unnamed publishers - #4969

Merged
kixelated merged 11 commits into
mainfrom
quest/m0/broadcast-epoch/hop-removal
Oct 8, 2026
Merged

kixelated merged 11 commits into
mainfrom
quest/m0/broadcast-epoch/hop-removal

Conversation

@kixelated

@kixelated kixelated commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Epochs (#4942) now carry publisher identity, but two leftovers still name a publisher through its hop: relays stamp a random per-session hop in front of an unnamed publisher's chain, and moq --hop pins the Hop ID a redundant pair was keyed on (with --cluster-id falling back to it).

Quest: quest/m0/broadcast-epoch/hop-removal.md (deleted here).

Approach

  • Relays stop stamping. Hops::stamp, the per-session stamp in the lite and IETF subscribers, and stampHops in js/net are gone. An unnamed publisher keeps its leading 0 (an empty chain becomes [0]), so it still ranks anonymous. Request-time exclusion is untouched.
  • moq --epoch. An optional UUIDv7 (MOQ_EPOCH) announced by the stages that publish once per run: stdin containers, capture, HLS import, archive replay, WHEP pull, and transcode output. Omitted, moq mints one per run. It is refused where it would be ignored: the RTMP, SRT, and WHIP ingests and import ts --program all (which announce their own), and an invocation with no publisher.
  • --hop / MOQ_HOP are removed outright, as are the hidden --origin alias (already refused in the release). --hop is now an unknown flag and MOQ_HOP is no longer read. --cluster-id no longer reads --hop; a plain publisher keeps the random per-process Hop ID its origin already declares.
  • Drafts (lite-07 and cluster-02 are in progress, edited in place): the stamping rules, the Stamping section, and their changelog bullets are removed, and a bridged upstream gets a single 0 again.
  • Docs: doc/bin/cli.md (Redundant publishers uses --epoch, notes --hop is removed), doc/setup/upgrade.md, doc/bin/relay/cluster.md, doc/concept/moq-lite.md.

Decisions

Maintainer decisions, 2026-10-07:

  1. --hop / MOQ_HOP handling
    • (a) keep a hidden flag that refuses startup with a migration to --epoch, per the rs/AGENTS.md released-flag rule
    • ✅ (b) delete it outright; docs say it is removed and --epoch replaces it. The maintainer chose this as an exception to the rs/AGENTS.md rule, accepting that a leftover MOQ_HOP is silently ignored.
  2. Where --epoch applies
    • ✅ (a) refuse it on the RTMP/SRT/WHIP gateways and import ts --program all, and loosen later without a breaking change
    • (b) let --connect pulls (SRT/RTMP/WHEP) take it now for same-epoch importers
    • (c) ignore it silently

Impact

  • CLI: removes --hop, MOQ_HOP, --origin, MOQ_ORIGIN; adds --epoch / MOQ_EPOCH; --cluster-id stops reading --hop. A deployment still setting MOQ_HOP runs, but each process mints its own epoch, so whenever either member of a redundant pair starts or restarts, its epoch replaces the other's broadcast and restarts its viewers until the pair moves to MOQ_EPOCH.
  • Wire: relays no longer insert a random hop in front of a route whose chain is empty or starts with 0; it keeps a single 0 (lite) or [0] HOP_PATH (moq-transport without the cluster extension). No message format changes.
  • Rust API: Hops::stamp was pub(crate); no public Rust or JS API change.

Restart model (#5012)

The docs describe a replaced epoch as restarting viewers, which holds both today (subscriptions end with Unroutable) and under the planned sticky subscriptions plus Restart announce. Whichever of this PR and #5012 lands second drops the other's link to hop-removal.md (deleted here; restart.md in #5012 lists it under Related).

Alternatives

  • Keep --hop as a hidden flag refused with a migration. Rejected by the maintainer (decision 1).
  • Apply --epoch to the gateways too. Deferred: they announce per connection (see the Gateways quest), so a fixed epoch would turn an encoder reconnect into a resume.

Follow-ups

  • Same-epoch importers (quest/m1/hop-aligned-import.md) may want --epoch on SRT/RTMP --connect pulls once they produce aligned groups; refusing it now keeps that a non-breaking loosening.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 5 commits October 6, 2026 23:15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Identity is the epoch's job now, so a route that names no publisher keeps its anonymous 0 and nothing goes in front of it. Removes Hops::stamp, the per-session stamp in both subscribers, stampHops in js/net, and the legacy_reconnect test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq announces under --epoch (a UUIDv7) when given, so a redundant pair shares one, and mints a fresh epoch per run otherwise. --hop and MOQ_HOP are refused with a migration to --epoch; the hidden --origin alias is gone. --cluster-id no longer falls back to --hop, and a plain publisher keeps the random per-process Hop ID its origin already declares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lite-07 and cluster-02 lose the stamping rule and changelog bullets: a received 0 is forwarded unchanged and a bridged upstream gets a single 0. The CLI docs describe --epoch for redundant publishers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: quest complete, left as a draft for maintainer decisions.

Checks: just check is green except four moq-uring tests that fail on RLIMIT_MEMLOCK (io_uring memory shared with other agents on this host; moq-uring is untouched here). cargo nextest on the whole workspace: 6283 passed, the same four failed. js/net 1232 tests pass. just drafts check passes. just test interop --all passes the full matrix.

Open decisions:

  1. --hop handling. The quest said moq "loses" --hop/MOQ_HOP. I kept them as a hidden flag that refuses startup with --hop / MOQ_HOP -> --epoch / MOQ_EPOCH, following the rs/AGENTS.md rule for released flags, because MOQ_HOP would otherwise be silently ignored. --origin is removed outright, since the release already refuses it. Options: (a) keep the refusal (recommended), (b) delete --hop as an unknown flag, which leaves MOQ_HOP silently ignored.
  2. Where --epoch applies. It is refused for the RTMP/SRT/WHIP ingests, import ts --program all, and invocations with no publisher, because those mint their own epoch or publish nothing. Options: (a) refuse for now and loosen later without a breaking change (recommended), (b) let --connect pulls (SRT/RTMP/WHEP) take it now for same-epoch importers, (c) ignore it silently.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 7, 2026 08:50
# Conflicts:
#	js/net/src/ietf/subscriber.test.ts
#	js/net/src/lite/subscriber.test.ts
#	quest/m0/broadcast-epoch/README.md
#	rs/moq-net/tests/legacy_reconnect.rs
The maintainer chose deletion over a hidden refusing flag: --hop is now an
unknown flag and MOQ_HOP is no longer read. The upgrade notes and CLI docs
say so, since a deployment still keyed on MOQ_HOP mints an epoch per process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a33e12cb-1e96-4d4d-9468-b1cb965f9e01
📥 Commits

Reviewing files that changed from the base of the PR and between fe0113f and 7ca9c79.

📒 Files selected for processing (35)
  • doc/bin/cli.md
  • doc/bin/relay/cluster.md
  • doc/concept/moq-lite.md
  • doc/setup/upgrade.md
  • drafts/draft-lcurley-moq-cluster.md
  • drafts/draft-lcurley-moq-lite.md
  • js/net/src/hop.ts
  • js/net/src/ietf/subscriber.test.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/lite/subscriber.test.ts
  • js/net/src/lite/subscriber.ts
  • js/net/src/origin.test.ts
  • quest/m0/broadcast-epoch/README.md
  • quest/m0/broadcast-epoch/hop-removal.md
  • quest/m0/broadcast-epoch/unepoched-takeover.md
  • quest/m1/cluster-routing/README.md
  • quest/m1/hop-aligned-import.md
  • quest/m1/lite07-finalize.md
  • rs/moq-cli/src/announced.rs
  • rs/moq-cli/src/archive.rs
  • rs/moq-cli/src/args.rs
  • rs/moq-cli/src/complete.rs
  • rs/moq-cli/src/fetch.rs
  • rs/moq-cli/src/hls.rs
  • rs/moq-cli/src/main.rs
  • rs/moq-cli/src/publish.rs
  • rs/moq-cli/src/rtc.rs
  • rs/moq-cli/src/transcode.rs
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/lite/setup.rs
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/src/server.rs
  • rs/moq-net/tests/legacy_reconnect.rs
  • rs/moq-relay/tests/cluster_unknown.rs
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Grok review of 357e5ff4 (first review; the earlier pushes were made while the PR was a draft)

The stamp removal is clean and keeps Rust and JS in sync. An unnamed chain still ends up as at least [0] in both lite subscribers. IETF adverts can't arrive empty, because HOP_PATH decode rejects an empty list (ietf/cluster.rs, js/net/src/ietf/cluster.ts:295). Un-epoched routes already never swap (model/origin.rs:4285), so the stamp wasn't protecting any resume. The --epoch plumbing reaches every once-per-run announce, and validate() sees publish as import because current() runs first. I found no blocking issues in the code. The notes below are mostly about docs accuracy.

Non-blocking

  1. The redundant-pair example in doc/bin/cli.md:391-402 shows a setup that is known to break today. Two import ts < feed.ts sharing one --epoch is exactly the 1+1 setup from A --hop standby that connects to the relay its subscribers are on ends every one of them with not found #4352 (a standby that connects aborts every subscriber with not found) and export ts exits with "frame timestamp is below the live edge" when the relay switches to a same-hop standby quickly #4354 (each importer numbers groups from its own counter, so export ts exits after a failover). Both are still open and are tracked by quest/m1/hop-aligned-import.md. The doc says the pair "must produce identical tracks with aligned groups", but the example implies import ts already does. Either add a caveat linking those issues and the quest, or say that no first-party importer meets the contract yet.
    The PR also deletes the sentence that a restarted encoder whose groups restart from 0 makes viewers wait. That is still true for a pinned epoch: a pair member restarted with the same MOQ_EPOCH rejoins as the same content, while its groups start over at 0. It's worth keeping a scoped version of that warning.
  2. doc/setup/upgrade.md:54-55 understates what a leftover MOQ_HOP does. Each process mints its own epoch, and the newest epoch wins the path and ends in-flight subscriptions to the old one with Unroutable (model/origin.rs, the request_broadcast doc right after line 4285). So starting or restarting the standby cuts viewers off the primary. That's worse than "stops failing over seamlessly". Since the maintainer picked option 1(b), the upgrade note is the only signal operators get, so I'd say this plainly there.
  3. Epoch parsing is strict, so the documented one-liner isn't portable. Epoch::from_str (rs/moq-net/src/epoch.rs:53-60) only accepts canonical lowercase hyphenated UUIDv7. uuidgen -7 only exists in util-linux 2.41+. macOS uuidgen has no -7 and prints uppercase, which would be refused. Consider lowercasing the --epoch value in the CLI before parsing, or mention the format and a portable generator in cli.md.
  4. An un-epoched reconnect is now invisible on the announce cursor. reconnecting_peer_joins_the_front_it_replaces (ietf/subscriber.rs:5357) asserts that nothing is emitted when the stale session drops. Before this PR, the stamp change at least surfaced an Update. Tracks served through the old session still end, because un-epoched routes never swap, so a consumer that only re-requests on announce events gets no signal. This fits the epoch design, but quest/m0/broadcast-epoch/README.md says older wires "see a restart as an end and start at the same path". That only holds when the old session retires before the new one announces. A one-line qualifier there would keep the quest accurate.
  5. Test gap (nit). epoch_applies_only_to_a_once_per_run_publisher covers the validation, but nothing checks that an explicit --epoch actually ends up on an announced route (every caller in tests passes Epoch::mint()). One assertion on a stdin or archive import's announced route.epoch would pin the plumbing at main.rs:530.

CI: queued on 357e5ff4. It was fully green on c873003c, and this push only merges main.

Verdict: MERGE once CI is green. Items 1 and 2 are cheap doc fixes worth making first.

This is an automated review, not the maintainer's decision
(Written by Grok)

…link to the finished quest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

On the Grok review of 357e5ff4, addressed in 27f2f85:

  1. Fixed: doc/bin/cli.md now says no importer guarantees aligned groups yet (A --hop standby that connects to the relay its subscribers are on ends every one of them with not found #4352, export ts exits with "frame timestamp is below the live edge" when the relay switches to a same-hop standby quickly #4354), and keeps a scoped warning that a member restarted with the same epoch whose groups restart from 0 makes viewers wait.
  2. Fixed: doc/setup/upgrade.md (and the PR's Impact) say plainly that with a leftover MOQ_HOP, each start or restart replaces the other member's broadcast and ends its viewers' subscriptions.
  3. Documented the format instead: --epoch takes a lowercase, hyphenated UUIDv7, such as util-linux uuidgen -7 prints. Lowercasing in the CLI would accept a spelling the wire and MOQ_EPOCH readers elsewhere refuse; strict is the fail-loud choice.
  4. Qualified the quest README ("once the old session retires"). The un-epoched handover itself is quest/m0/broadcast-epoch/unepoched-takeover.md, which landed on main meanwhile; I dropped its link to the deleted hop-removal.md so quest check passes.
  5. Leaving the plumbing test out: epoch_applies_only_to_a_once_per_run_publisher covers validation, and the announce path is shared with the minted case the tests already exercise.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 27f2f85

No new actionable defect found. The direction is coherent: publisher epochs replace hop-based content identity, while session-origin exclusion remains separate; Rust and JS preserve anonymous zero hops instead of manufacturing a publisher ID. Reviewed the CLI epoch propagation/validation, subscriber changes, regression updates and migration documentation. The current docs explicitly warn about ignored MOQ_HOP and the lack of importer group alignment (doc/setup/upgrade.md:50–59; doc/bin/cli.md:393–418), addressing the earlier operational concerns. Verification limits: static review only; no CLI, mesh, or interoperability tests run. This does not establish seamless redundancy for importers that do not align groups.

kixelated and others added 2 commits October 7, 2026 13:38
A leftover MOQ_HOP restarts the other member's viewers whether subscriptions
end (today) or stay sticky behind a Restart announce (#5012), and an older
version's end-and-start no longer waits on the old session in the quest goal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Auto-merge enabled at 7ca9c79a03891e89b2effcb62cfa0f7ce36183d1.

  • Review: the OpenAI review of 27f2f85f found nothing actionable. Since then there is a conflict-free main merge and 7ca9c79a, a two-line docs wording edit (doc/setup/upgrade.md, the broadcast-epoch quest README), which the maintainer treats as trivial.
  • Merges cleanly with current main; the gateway, CLI, relay and moq-net crates cargo check --tests and quest check passes on that merge.
  • Decisions: as recorded in the PR body. quest: plan Restart, Rust untimed default, and three follow-ups #5012's links to hop-removal.md are dropped by whichever of the two lands second.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit ba79d5f into main Oct 8, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m0/broadcast-epoch/hop-removal branch October 8, 2026 01:35
kixelated added a commit that referenced this pull request Oct 8, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 8, 2026
Resolve the doc conflicts by keeping the feature-level pages and carrying
over what main changed underneath them: per-connection ingest epochs and
`--epoch` (#4962, #4969), no random first hop for unnamed publishers,
untimed tracks, the bounded HLS window, disabled renditions, the audio
group duration and Balanced default preset, and the Go pointer timestamp.

Also address the open review notes: scope restart takeover to moq-lite 07
and today's Unroutable behavior, keep `--cluster-tier` covering admitted
LAN peers, qualify the watch buffer as cheap for audio only, and say an
over-budget JSON append is refused before anything is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant