Skip to content

Draft-20 backlog: documentation cleanup and relay refusal codes - #117

Merged
floatdrop merged 7 commits into
draft-20from
draft20-backlog-relay-codes
Sep 27, 2026
Merged

floatdrop merged 7 commits into
draft-20from
draft20-backlog-relay-codes

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

This PR clears two items from the draft-20 backlog: documentation cleanup and relay refusal-code fixes. There is one commit per item. Each was reviewed by moqt-reviewer, and each code change has tests that were seen failing first, either before the fix or under a mutant that removes the behaviour.

Documentation

  • e22125d STATUS.md and the namespace handle docs now match the code and draft-20:
    • The Limitations entry saying duplicates aren't compared is gone, because the relay does compare them.
    • LOC PropAudioLevel is 0x0C, not 0x0A.
    • The "handles the application reads itself" note now separates role checks from scope checks, and names the handles that have no broker.
    • The request-stream GOAWAY note now says such a GOAWAY is checked and otherwise ignored.
    • The FILL_PARAMETERS row is now DONE, and the INCLUDE_PROPERTIES row points at the Group Order draft gap.
    • The Joining FETCH mention (removed in draft-20) is dropped.
    • NamespacePublication / IncomingNamespacePublication docs: NAMESPACE / NAMESPACE_DONE answer only a SUBSCRIBE_NAMESPACE (§10.17, §10.18). A namespace is withdrawn by cancelling the request, not by a FIN (§6.2, §3.3.2). The docs now also name the broker settings a reader needs.
  • 9a2c992 corrects 13 stale § citations in code comments, each checked against the draft text: padding, GREASE, datagram flags, fetch ordering, caching, draining, and relaynet ALPN. Three of the old review's suggested corrections were themselves wrong and were left as they were.
  • d12096b records one interpretation: a datagram is filtered as Subgroup 0 under SUBGROUP_FILTER (§2.2, §5.1.4).

Relay

  • 33dd402 When an upstream answers a SUBSCRIBE with GOING_AWAY, the subscriber now gets GOING_AWAY with its Retry Interval, instead of INTERNAL_ERROR. It also counts as "no publisher yet" for RENDEZVOUS_TIMEOUT.
  • 38f0a53, dbaaa73 A SUBSCRIBE's refusal no longer depends on the order candidates answer in:
    • Ranking: unknown Mandatory Track Property, then malformed Track Properties, then any other refusal, then GOING_AWAY, then TIMEOUT, then DOES_NOT_EXIST or a transport failure.
    • Ties go to the soonest retry, then to a specific code over INTERNAL_ERROR.
    • Retry 0: an upstream GOING_AWAY with Retry Interval 0 gets the relay's own ~1s instead.
    • Transport failures don't end a RENDEZVOUS_TIMEOUT hold, and that candidate is asked again on the next look. Lack of stream credit is excluded: the hold ends on it as before.
  • 5e61d8a TRACK_STATUS is forwarded upstream when the track has no Established subscription (§10.15 MAY):
    • Every candidate SUBSCRIBE would try is asked, concurrently.
    • Answers combine as the relay's SUBSCRIBE_OK would: the entry's Track Properties, else the first answer's, and the largest LARGEST_OBJECT.
    • If none answers OK, the subscriber gets the refusal SUBSCRIBE would give.
    • Forwarding counts against MaxSubscriptionsPerSession (§13.1) and is bounded at 5s (§13.6).
    • Loop guard as for SUBSCRIBE and stitch FETCH.

Decisions and interpretations

  • Maintainer decisions:
    • forward TRACK_STATUS, asking every candidate and taking the maximum;
    • cap plus 5s timeout;
    • precedence GOING_AWAY > TIMEOUT > DOES_NOT_EXIST;
    • pass an upstream GOING_AWAY through, with 0 replaced by the relay's retry;
    • soonest retry wins ties;
    • transport failures count as "no publisher yet".
  • Interpretation, marked in the code: GOING_AWAY outranks §10.2.6's DOES_NOT_EXIST for a known publisher that is draining.
  • Behaviour changes:
    • A namespace-only TRACK_STATUS no longer gets an immediate empty OK. TestTrackStatus_ReplyEmptyPropertiesForKnownNamespace pinned that and is removed.
    • TestSubscribe_UpstreamRejects_PropagatesRejection's GOING_AWAY row changes from INTERNAL_ERROR to GOING_AWAY.

Known gaps (added to STATUS.md)

  • Concurrent TRACK_STATUS requests for one track aren't coalesced.
  • A SUBSCRIBE the relay can't open for lack of stream credit is answered DOES_NOT_EXIST with no retry; EXCESSIVE_LOAD would fit better (§10.6.2).

Verification

  • go test ./... and golangci-lint run pass.
  • go test -race ./pkg/relay/... passes.

🤖 Generated with Claude Code

floatdrop and others added 7 commits September 27, 2026 21:44
…code and draft-20

STATUS.md:
- drop the Limitations entry saying duplicate Objects are not compared (the
  relay compares them, as the Malformed tracks entry describes);
- LOC property IDs are those draft-ietf-moq-loc-04 requests from IANA;
  PropAudioLevel is 0x0C, not 0x0A;
- "Handles the application reads itself": `CheckPeerParams` checks
  parameter scope, not roles; name the handles with no broker, and note that
  a bare `NewRequestBroker` checks only what it is configured to;
- "Inbound GOAWAY": a request-stream GOAWAY is checked (§10.4) and otherwise
  ignored, with no re-issue;
- row 10.2.15 FILL_PARAMETERS is DONE (Range Filters are inherited, §5.1.3),
  and notes that SUBSCRIBER_PRIORITY inside it has no effect; row 10.2.21
  points at the Group Order draft gap;
- the package summary no longer lists relative/absolute Joining FETCH,
  removed in draft-20;
- remove the fixed items from the Documentation backlog.

pkg/moqt/session/namespace.go: NAMESPACE / NAMESPACE_DONE answer only a
SUBSCRIBE_NAMESPACE (§10.17, §10.18), so they are not follow-ups on a
PUBLISH_NAMESPACE stream. That stream carries the announcer's REQUEST_UPDATE
and GOAWAY. A publication is withdrawn, or its acceptance revoked, by
cancelling the request (§6.2, §3.3.3), not by a FIN (§3.3.2). The
IncomingNamespacePublication doc now names the broker settings a reader needs
(PeerMessages, UpdateScope, HandleUpdates).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment-only: no behaviour or test changes. Each citation and quote was
checked against the draft-ietf-moq-transport-20 text.

- session/datagram.go: PADDING datagrams are §11.5.2, not §11.3.
- message/subgroup.go: an unknown stream type also closes the session
  (§3.4); it is not ignorable GREASE.
- message/datagram.go: drop the stale "Figure 24" ranges and a quote
  absent from -20, and restate the §11.3.1 invalid Type Flags list.
- session/datastream_in.go: an omitted FETCH GROUP_ORDER means Ascending
  (§10.2.8); a fill fetch stream inherits the subscription's order
  (§10.2.15).
- message/grease.go: quote §14 accurately, and drop a wrong §1.4.3 cite.
- message/types.go: §10 names no error code for an unknown message type.
- relay/handler_subscribe.go: a Next Object upstream (§5.1.2) cannot
  serve downstream ranges that start before Largest Object.
- session/options.go: Setup Options are §1.4.3 Key-Value-Pairs, so
  delta encoding fixes their order (not §10.2).
- relay/cache/cache.go: in-group Object ID order is §10.13, not §11.4.3.
- relay/handler_fanout.go: §9.7 says nothing about draining; the reason
  is the cache (§9.1).
- relay/handler_datagram.go: §5.1.4 does not define a datagram as
  Subgroup 0 (§2.2: it belongs to no Subgroup); mark it relay policy.
- relay/handler_forward.go: "no PUBLISH_DONE after REQUEST_ERROR" is
  §5.1/§5.1.1, not §3.3.3.
- relay/relaynet/relaynet.go: the ALPN comment referred to draft-19.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…se the documentation backlog

The citation pass (9a2c992) made explicit that the relay filters a datagram
as Subgroup 0 under SUBGROUP_FILTER: §2.2 says a Datagram Object belongs to
no Subgroup, and §5.1.4 does not cover it. This records it among the
Limitations' interpretations. The Documentation backlog is done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An upstream that answered the relay's SUBSCRIBE with GOING_AWAY, as an
upstream relay draining does before its GOAWAY reaches this one, was passed
on as INTERNAL_ERROR. The relay answers a draining publisher it knows about
with GOING_AWAY itself, so the subscriber's answer depended on timing.

upstreamRejection now passes GOING_AWAY through with its Retry Interval,
alongside DOES_NOT_EXIST, TIMEOUT, EXCESSIVE_LOAD and UNSUPPORTED_EXTENSION.
awaitsPublisher counts it as "no publisher yet", so a RENDEZVOUS_TIMEOUT hold
continues (§10.2.6).

TestSubscribe_UpstreamRejects_PropagatesRejection's GOING_AWAY row, which
pinned the old mapping, now expects GOING_AWAY with the Retry Interval.
TestRendezvous_UpstreamGoingAwayKeepsHold is new. Both fail before this
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es does not matter

When every SUBSCRIBE candidate only said the track has no publisher yet, the
last to answer set the refusal code. So a draining local publisher plus a
remote DOES_NOT_EXIST gave DOES_NOT_EXIST, and the reverse gave GOING_AWAY.

candidateErrRank now orders the candidates' errors, and the highest is kept
whatever the order:
1. a Track Properties refusal (§2.5.1);
2. any other refusal, which ends a RENDEZVOUS_TIMEOUT hold;
3. then the most actionable "no publisher yet" answer: GOING_AWAY (retry,
   §10.6.2), then TIMEOUT, then DOES_NOT_EXIST.

The ranking is the maintainer's choice. It carries over the earlier reading
that GOING_AWAY outranks §10.2.6's DOES_NOT_EXIST for a publisher that is
known but draining. A relay skipped for draining ranks through the same
function.

TestSubscribe_NoPublisherYetRanking covers each pair in both orders. The
three orders where the higher-ranked answer comes first failed before this
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…blished subscription (§10.15)

The relay "treats [TRACK_STATUS] identically as if it had received a
SUBSCRIBE" (§10.15), but without an Established subscription it answered
without asking upstream. It sent an empty TRACK_STATUS_OK for any track under
an advertised namespace, and an OK from a leftover entry whose publisher had
left. SUBSCRIBE would go upstream in both cases, and might fail.

§10.15 lets such a relay "forward TRACK_STATUS to one or more publishers";
the maintainer chose to. trackStatusUpstream asks every candidate SUBSCRIBE
would try, concurrently: local publishers of a covering namespace, then
Discovery relays, skipping draining ones (§10.4). The answers combine as the
relay's SUBSCRIBE_OK would:
- Track Properties: the entry's, else the first answer's (§9.6);
- LARGEST_OBJECT: the largest of all answers and the entry's (§10.2.17).
With no OK, the refusal is the one SUBSCRIBE would give (candidateErrRank,
upstreamRejection), or DOES_NOT_EXIST with no candidate.

Bounds, also the maintainer's choice:
- a forwarding TRACK_STATUS counts against MaxSubscriptionsPerSession
  (§13.1);
- the forwarding is bounded at 5s, and a candidate that times out counts as
  TIMEOUT (§13.6);
- all of it ends on the requester's STOP_SENDING.
Relay policy, as for SUBSCRIBE and stitch FETCH: the requester's own session
is not asked while a TRACK_STATUS for the track to it is in flight (a loop,
§6.2). The registry's in-flight counter is now keyed by request type
(BeginRequest / RequestPending).

TestTrackStatus_ReplyEmptyPropertiesForKnownNamespace pinned the old empty
OK and is removed. TestRelay_TrackStatusRequestUpdateClosesSession's
publisher now answers the forwarded request.

New tests in track_status_forward_test.go, each red before the change, or
under a mutant removing its behaviour: forwarding with and without
INCLUDE_PROPERTIES, refusals passed on, a leftover entry, a Discovery remote,
the loop guard, every candidate asked (the largest of two; one refusing), an
unknown Mandatory Track Property, a draining candidate, a silent one timing
out, the requester's cancel, and the subscription cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, Retry Interval included

Follow-ups to 33dd402 and 38f0a53. The maintainer decided the first three:
- An upstream GOING_AWAY with Retry Interval 0 ("SHOULD NOT be retried",
  §10.6.2) gets the relay's own jittered ~1s. Relay policy: the 0 is taken
  to speak for the upstream's own draining hop.
- preferCandidateErr replaces rank-only comparison, for SUBSCRIBE and
  forwarded TRACK_STATUS alike. Of equal rank, the answer allowing the
  soonest retry wins, and 0 only when every answer says so. Of equal rank
  and retry, a specific code beats INTERNAL_ERROR, then the lower code
  wins. The jittered interval compares at its top.
- A candidate whose request failed at the transport, with no answer (a
  reset, a FIN, its session ending), counts as "no publisher yet", with
  DOES_NOT_EXIST, the code it is answered with. It no longer masks another
  candidate's GOING_AWAY or TIMEOUT, and a RENDEZVOUS_TIMEOUT hold goes on
  and asks it again on the next look, never twice in one. Not a request
  the relay could not open for want of stream credit: that publisher is
  there, so the hold ends as before (the answer there, DOES_NOT_EXIST with
  no retry, is on the STATUS.md backlog).
- An unknown Mandatory Track Property outranks Track Properties that do not
  parse, so a subscriber gets §2.5.1's UNSUPPORTED_EXTENSION whatever the
  order.

Tests in session_upstream_test.go, each red before the change or under a
mutant removing its behaviour: UpstreamGoingAwayWithoutRetry,
TiedRefusalsSoonestRetry, OtherRefusalTieIsOrderFree,
TransportFailureIsNoPublisherYet, UnsupportedMandatoryPropertyOutranksMalformed,
and TestRendezvous_NoStreamCreditEndsHold / _TransportFailureAskedAgain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@floatdrop
floatdrop merged commit 72cf83c into draft-20 Sep 27, 2026
11 checks passed
@floatdrop
floatdrop deleted the draft20-backlog-relay-codes branch September 27, 2026 17:34
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