Conversation
afrind
left a comment
There was a problem hiding this comment.
The overall direction seems ok. Some questions/feedback. I think the most substantive bit is around wether we want to use random numbers to synthesize uniqueness, or use the named relayID_ param we already have as a seed. We could also pad it out to some minimum length by repeating it, then hash? It should be deterministic across restarts, I think?
@afrind reviewed 22 files and all commit messages, and made 14 comments.
Reviewable status: all files reviewed, 12 unresolved discussions (waiting on akash-a-n, mondain, and peterchave).
design/RELAY_HOPS_IMPLEMENTATION_PLAN.md line 1 at r1 (raw file):
# MOQT Relay Hops Implementation Plan
I don't think we should check in implementation plans
docs/draft-lcurley-moq-relay-hops.txt line 5 at r1 (raw file):
moq L. Curley
Move to docs/ref
docs/relay-hops.md line 34 at r1 (raw file):
```bash sudo deps/moxygen/standalone/install-system-deps.sh ./scripts/build.sh setup
I don't think the relay-hops guide needs to restate how to build and run, only how to deploy a mesh network
docs/relay-hops.md line 105 at r1 (raw file):
support on any other connection. ## Configure a Root Relay
Do you still need a root relay with hops? Do we need to repeat the entire config format
src/MoqxPicoRelayServer.cpp line 90 at r1 (raw file):
listenerCfg_(listenerCfg), context_(std::move(context)), evb_(ioExecutor->getAllEventBases()[0].get()) { addSetupParameter(SetupParameter(folly::to_underlying(SetupKey::RELAY_HOPS), std::string{}));
I wonder if we want to universally negotiate this parameter, of if we want to enable it via config. I'm open to hardcoding it now if there's good reason.
src/MoqxRelay.h line 122 at r1 (raw file):
) : relayID_(std::move(relayID)), relayHopID_(relayHopID == 0 ? moxygen::generateRelayHopID() : relayHopID),
moqx upstreams have string names. Rather than generate random IDs, perhaps we should hash the names?
src/MoqxRelay.h line 126 at r1 (raw file):
useLocalForwarders_(useLocalForwarders), maxDeselected_(maxDeselected), idleTimeout_(idleTimeout), activityThreshold_(activityThreshold) { XCHECK_LE(relayHopID_, moxygen::kMaxRelayHopID);
Is this less than 2^64-1? I guess if relay-hops runs on draft-16 it needs to be
src/MoqxRelay.h line 405 at r1 (raw file):
moxygen::TrackNamespace allowedNamespacePrefix_; std::string relayID_; uint64_t relayHopID_;
Yeah having two fields seems confusing.
src/MoqxRelay.cpp line 416 at r1 (raw file):
std::vector<uint64_t> relayHopPath; if (!session->isRelayHopsNegotiated()) { relayHopPath.push_back(session->getRelayHopSourceID());
This seems more like a "publisher ID" rather than a relay source ID, since it might not be a relay at all.
src/MoqxRelay.cpp line 418 at r1 (raw file):
relayHopPath.push_back(session->getRelayHopSourceID()); } else { const auto hopPathKey = folly::to_underlying(TrackRequestParamKey::HOP_PATH);
The next 5 lines or so probably warrant a helper in moxygen like getParamValue or something
src/MoqxRelay.cpp line 429 at r1 (raw file):
auto version = session->getNegotiatedVersion(); XCHECK(version.has_value()); auto decoded = decodeRelayHopPath(it->asString, *version);
This might be feedback for Luke -- is the hop path encoded canonically with QUIC varints or is it based on the underlying transport protocol version (which is what I think you have here).
src/MoqxRelay.cpp line 1574 at r1 (raw file):
if (node->publisherSession() && node->publisherSession() != session && (incomingPeerID.empty() || node->publisherPeerID() != incomingPeerID)) { if (excludesHop(excludeHop, node->relayHopPath(), relayHopID_)) {
You didn't start this fire, but this seems to be duplicated with the above section and we should see if we can use a helper to reduce duplication and boilerplate?
src/MoqxRelayContext.cpp line 50 at r1 (raw file):
MoqxRelay::kDefaultIdleTimeout, MoqxRelay::kDefaultActivityThreshold, relayHopID_
Reorder this param to be closer to relayID (unless we replace it with hash(relayID), then you don't have to pass the defaults here.
|
Addressed in 8df7fd4, with moxygen dependency commit 41f3bfb6. Hop IDs remain random because the draft requires opaque randomly chosen values that are stable for the endpoint lifetime, rather than deterministic across restarts. Generation and legacy-publisher ownership now live in moqx, while relayID remains the operational routing identity. |
|
Removed the checked-in implementation plan. |
|
Moved the draft to docs/ref/draft-lcurley-moq-relay-hops.txt and updated the guide link. |
|
Removed the duplicated build, basic run, container, and general configuration instructions. The guide now focuses on relay-hop negotiation, mesh and cycle deployment, rolling upgrades, and verification. |
|
Removed the root-relay and full configuration walkthroughs. Relay hops do not require a distinguished root; the revised guide describes only the upstream relationships needed for a mesh or cycle. |
|
Kept universal advertisement for now. Negotiation is bilateral and session-local, so the wire extension remains inactive with an unsupported peer, while advertising it consistently across listeners and upstream clients avoids configuration-dependent loop-safety gaps. Added comments and deployment guidance explaining that choice. |
|
Kept Hop IDs random rather than hashing relayID because draft-lcurley-moq-relay-hops requires each relay and origin publisher to choose an opaque random Hop ID. relayID remains the operational string identity; relayHopID is stable for the process context lifetime and may change after restart as the draft permits. |
|
Moved the bound into moqx and defined it as 2^62 minus 1, matching the maximum QUIC variable-length integer used by draft 16. Generation masks to that bound and rejects zero. |
|
Kept both fields because they have different contracts: relayID is an operational string used for authentication and upstream routing, while relayHopID is the opaque random protocol loop identifier. Added comments at the declarations and moved Hop ID generation fully into moqx to make the ownership explicit. |
|
Removed getRelayHopSourceID from moxygen. moqx now assigns each non-negotiating publisher session a stable random legacy-publisher Hop ID, stores it with a weak session reference, and prunes expired entries. |
|
Added Parameters::getFirstParam in moxygen and use it here for HOP_PATH and EXCLUDE_HOP lookup. |
|
The encoding is version-aware: draft 16 uses QUIC variable-length integers and draft 17 or later uses the MOQT variable-integer encoding. encodeRelayHopPath and decodeRelayHopPath dispatch from the negotiated MOQT version, and the tests cover drafts 16, 17, and 18. |
|
Extracted shouldForwardNamespace and now use it for both immediate forwarding and replay of existing namespaces. It centralizes source-session suppression, subscription options, and EXCLUDE_HOP filtering. |
|
Moved relayHopID next to relayID in the MoqxRelay constructor and updated internal call sites, eliminating the need to pass every intervening default. The public MoqxRelayContext argument order remains unchanged to preserve source compatibility. |
Summary
draft-lcurley-moq-relay-hopsnamespace path ingestion and forwardingEXCLUDE_HOPWhy
Namespace advertisements can circulate indefinitely when connected relays form a cycle. The negotiated hop path lets each relay recognize its own returning advertisement without requiring centrally assigned identifiers or a YAML topology map.
Deployment impact
Relay-hops negotiation is automatic and session-local; there is no new YAML toggle. Non-negotiated peers retain legacy behavior. Any graph segment containing a legacy relay must remain acyclic because that relay cannot preserve the path.
Each service still supports a single upstream. Multi-upstream path selection and failover remain deferred.
Dependency
Validation
relay_chainintegration testrelay_hops_cycleintegration testvalidate-config --strict_config./scripts/format.sh --checkgit diff --check origin/main..HEADThis change is