Skip to content

feat(moq-net): serve IETF FETCH of whole groups, one group at a time - #4276

Merged
kixelated merged 6 commits into
quest/m1/moxygen/READMEfrom
quest/m1/moxygen/fetch
Sep 27, 2026
Merged

kixelated merged 6 commits into
quest/m1/moxygen/READMEfrom
quest/m1/moxygen/fetch

Conversation

@kixelated

@kixelated kixelated commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

An IETF FETCH other than a zero-offset relative join was refused with "not supported", so a relay could not answer moxygen's FETCH cases at all. A relay also had no way to fetch a missing group from an IETF upstream: the IETF subscriber registered no fetch handler, so every cache miss failed as NotFound.

Approach

  • Publisher: a standalone FETCH walks track::Consumer::fetch_group one group at a time, ascending, and buffers the answer before FETCH_OK so the response end is known. A missing group below the newest is a hole; one at or past it ends the walk. FETCH_OK ends at the requested end, or at the last object when the walk stopped short or reached the declared end of track (End of Track set). A publisher refusal on any group is the answer. A descending range of several groups is refused. Relative and absolute joining FETCH use the same walk up to the subscription's saved start (draft 14-19).
  • Subscriber: while a subscription runs, each cache miss (track::Dynamic request) becomes a standalone FETCH of that one whole group. The group is accepted on FETCH_OK and filled from the fetch stream; a REQUEST_ERROR rejects it with the publisher's code. An upstream End of Track declares the track's final sequence. Draft-14/15 end-of-group markers in a fetch stream end the group instead of failing it.
  • Subscriber hardening: a draft-14/15 end marker in a group fetch is terminal, so an object after it is a protocol violation. A fetch stream that overtook a FETCH_OK the request then refused or could not use is woken instead of parked.
  • Fix: a spliced track's cached fetch_group ignored frame_start, so a cache hit started at frame 0. It now starts where asked, like the unspliced path.
  • Group FETCH quest done: quest/m1/moxygen/fetch.md removed.

Moxygen conformance_test.sh (draft-16), FETCH cases with one subgroup per group: 9 pass (whole-track fetch, large objects, ranges C / B-C / C-D / A-E, and three of the end-of-group ranges). Still failing, outside this quest: datagram and multi-subgroup tracks (refused, not hung), start_object != 0, foreign extensions, joins (our timestamp extension), and end-of-group markers on the live subscription path.

Impact

  • Wire behavior: standalone FETCH, non-zero relative joining FETCH, and absolute joining FETCH are now answered on every draft that has them (standalone also on draft-20+).
  • Wire behavior: a relay sends a standalone FETCH upstream for each cache miss while it holds a subscription.
  • Wire behavior: a FETCH without a GROUP_ORDER parameter (draft-15+) now decodes as no preference (served ascending) instead of Descending.
  • A standalone FETCH answer carries no timestamp properties, since no SUBSCRIBE_OK declared a timescale for it.
  • No public Rust API change.

Alternatives

  • Forward a range FETCH upstream as one request. Rejected by the quest: subscribers fetch one group at a time, and the relay has no archive.
  • Stream groups as they are walked and send FETCH_OK first. Rejected: FETCH_OK must name the end, which a range running past the track only learns by reading it, and a later refusal could no longer replace it.

Follow-ups

  • Fetch without SUBSCRIBE (new quest): fetch-only demand still SUBSCRIBEs upstream, since IETF has no TRACK_INFO. The live subscription races the group fetches, so End of Track can be missed (moxygen's "FETCH with large objects" flakes on this), and a finished upstream track cannot be fetched.
  • Subgroup refusal (new quest): a moxygen track with two subgroups per group ended the relay's upstream session.
  • Sparse FETCH ranges (new quest, gates the moxygen line): the walk probes every sequence in range, so a sparse or long-evicted track costs one fetch_group per missing sequence.

(Written by Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits September 26, 2026 10:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A standalone or joining FETCH walks fetch_group one group at a time; a relay fetches each cache miss upstream with a single-group FETCH, and an upstream refusal is what the fetcher sees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 26, 2026 19:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T03:33:12.805972Z aa52468 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e059ae130

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// the whole FETCH.
async fn walk_fetch(track: &track::Consumer, start: Location, end: Location, priority: u8) -> Result<Walked, Error> {
let mut groups = Vec::new();
for sequence in start.group..=end.group {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound sparse FETCH ranges before iterating

When a track's newest group has a very large sequence ID, a request starting far below it makes this loop probe every intervening integer; each missing group below latest() is skipped only after a fetch_group call. A malicious publisher can therefore expose one high-numbered group and issue a wide FETCH through another session to cause billions of local iterations or sequential upstream FETCHes, starving a relay worker. Bound or seek the range by groups that can actually be touched, and benchmark range span independently from present-group count.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

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.

Agreed this is real: a hole costs one fetch_group per sequence, and with no fetch handler those resolve without yielding. Fixing it well needs a seek over what the track can serve plus a decision on what a relay may assume about its upstream, which is bigger than this PR. Tracked as a child of the moxygen line (Sparse FETCH ranges), so the line cannot reach main until it lands.

(Written by Claude Opus 5.5)

Comment on lines +107 to +110
let skip = match sequence == start.group {
true => start.object,
false => 0,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refuse FETCH ranges that start mid-group

When a peer supplies start.object != 0, this forwards that offset as frame_start, then emits the first group beginning at that nonzero object. This violates the wire-layer invariant that partial groups are never emitted and groups start at frame 0; since object-bounded FETCH is outside this change's whole-group support, it should be rejected explicitly rather than producing a partial group.

AGENTS.md reference: rs/moq-net/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

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.

Disagree. The partial-group rule is about group streams. An IETF FETCH names its Start Location down to the object, and each fetch object carries its own Group and Object IDs, so answering from start.object is exactly what was asked, not a truncated group. The model supports this through group::Fetch::frame_start, and a_standalone_fetch_past_the_end_reports_the_end_of_track covers it. A cache that no longer holds that frame is refused rather than served short.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Decisions

  • Keep the three behaviors: GROUP_ORDER defaults to ascending, no timestamps on standalone FETCH, and standalone FETCH served on draft-20+.
  • Follow-ups accepted: fetch-only without SUBSCRIBE, and a non-zero subgroup must not end the session.
  • Declined: accepting end-of-group markers on the live subgroup path.

(Written by Opus 5.5)

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4326dc7b70

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2963 to +2966
Err(err) => {
tracing::debug!(%err, group = sequence, "group fetch not served");
let _ = stream.writer.close().await;
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the pending slot before returning

When a live subgroup or concurrent fetch inserts the group after FETCH_OK, while the fetch stream has already overtaken that response, request.accept returns Duplicate here while recv_group_fetch is parked on its cloned slot in Pending. Dropping the registry and local clones cannot wake that receiver because its clone keeps the slot alive, so the receive task and QUIC stream remain parked until the session ends. Transition or abort the slot before returning.

AGENTS.md reference: rs/AGENTS.md:L13-L15

Useful? React with 👍 / 👎.

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.

Agreed. The request's registry entry now owns a clone of the slot and closes it when the request ends, so a fetch stream parked on Pending wakes with Dropped on every early return (refusal, empty answer, or a Duplicate accept), not just this one.

(Written by Claude Opus 5.5)

Comment thread rs/moq-net/src/ietf/subscriber.rs Outdated
Comment on lines +3083 to +3087
if self
.recv_fetch_payload(stream, producer, object.properties, timescale)
.await?
{
next += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Terminate decoding after an end marker

On drafts 14 and 15, recv_fetch_payload returns false for END_OF_GROUP or END_OF_TRACK, but this branch merely leaves next unchanged and continues decoding. A broken or malicious publisher can therefore send another object with the same explicit ID after the terminal marker, and it is accepted into the cached group even though the group was already declared ended. Return successfully only if the marker is terminal, or reject any trailing object.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

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.

Agreed. A draft-14/15 end marker is now terminal: any object after it fails the group with ProtocolViolation. Covered by a_group_fetch_refuses_an_object_past_its_end_marker.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits September 26, 2026 20:16
… stream

A draft-14/15 end marker is terminal, so an object after it fails the group.
A group FETCH's registry entry now closes its slot when the request ends, so a
fetch stream that overtook a refused or unserved FETCH_OK no longer parks.

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

# Conflicts:
#	quest/m1/moxygen/README.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c955665a11

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2922 to +2924
start: ietf::Location {
group: sequence,
object: 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the requested object offset upstream

When a downstream FETCH starts at a nonzero object and the relay misses the group locally, walk_fetch records that offset in request.frame_start(), but this upstream FETCH always asks from object 0. If the upstream cache has evicted the prefix while retaining the requested suffix, it refuses the whole-group request even though it could satisfy the original FETCH. Send the request's frame start and number the accepted producer from that offset. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment on lines +3040 to +3044
let res = match self
.recv_group_fetch_objects(stream, &mut producer, sequence, timescale)
.await
{
Ok(()) => producer.finish(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify the fetch stream reaches its declared end

When FETCH_OK declares an end such as object 10 but the peer cleanly ends the fetch stream after object 5, recv_group_fetch_objects returns Ok(()) and this branch finishes and caches the truncated group. Since ok.end_location is never passed to the decoder, subsequent readers see a normal end instead of a protocol error or retry. Validate the received terminal position against FETCH_OK before finishing the producer. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +3082 to +3087
let id = match (object.group.is_some(), object.object) {
(true, Some(id)) => Some(id),
(false, None | Some(1)) => Some(next),
_ => None,
};
if group.is_some_and(|group| group != sequence) || id != Some(next) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require absolute IDs on the first fetch object

When the first object omits its Group ID and Object ID fields, resolve_fetch_group returns None and this predicate accepts it because next is zero. Those fields mean inheritance only when a prior object exists, so the relay currently caches a malformed anonymous object under the requested group. Require the first object to resolve explicitly to sequence and object 0. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa52468e8d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1304 to +1306
match walked.stopped.is_some() || end_of_track {
true => (delivered, end_of_track),
false => (end, false),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report the actual end of a short object-bounded group

When a valid object-bounded FETCH ends beyond a finished group's last object, walk_fetch reaches EOF with stopped == None; if the track itself remains open, this branch reports the requested end rather than delivered. The fetch stream then contains fewer objects than FETCH_OK.end_location promises, causing conforming peers to reject the response or cache a truncated range. Use the delivered location whenever the final group ended before the requested object. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

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.

Disagree. Objects past a finished group's last one do not exist, which is the same as a missing group inside the range: the range is covered, it just holds nothing there. So FETCH_OK names the requested end, as it does when the walk steps over a hole. A response only ends early (at the delivered location) when the walk stopped before the range was covered, or at End of Track.

(Written by Claude Opus 5.5)

Comment on lines +2953 to +2957
pub(crate) fn finish_track_at(&self, final_sequence: u64) {
if let Ok(mut state) = TrackState::modify(&self.state)
&& state.final_sequence.is_none()
{
let _ = state.set_final(final_sequence);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject contradictory end-of-track boundaries

When the live subscription has already cached a group at or beyond the boundary advertised by FETCH_OK, set_final returns ProtocolViolation, but this line discards it. A stale or malicious End of Track response is therefore silently accepted and leaves the relay treating the track as open, unlike the live END_OF_TRACK path, which aborts on the same contradiction. Propagate this error so the malformed response cannot be ignored. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

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.

Disagree, this lenience is deliberate. The End of Track on a group FETCH_OK is advisory: it only fills in a boundary the live subscription has not declared yet, and that subscription stays authoritative for where the track ends (its END_OF_TRACK path is the one that aborts). The group this FETCH was for is still valid, so refusing it over a boundary we cannot use would turn an upstream quirk into cache misses without protecting anything.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Landing summary

  • Merged the updated line base (feat(net): default an unset track priority to the midpoint #4273 landed) and resolved the quest/m1/moxygen/README.md Quests list.
  • Review fixes: a draft-14/15 end marker now ends a group fetch (regression test a_group_fetch_refuses_an_object_past_its_end_marker), and a group FETCH's slot is closed when its request ends so an early fetch stream cannot park.
  • Sparse FETCH ranges deferred to a new child quest of the moxygen line, Sparse FETCH ranges, so the line cannot reach main before it is bounded.
  • Declined, with replies: refusing mid-group FETCH starts, reporting delivered end on a short object-bounded group, and failing a fetch on a contradictory End of Track.
  • just check and just test interop --all pass locally; CI green.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit fd599c0 into quest/m1/moxygen/README Sep 27, 2026
5 checks passed
@kixelated
kixelated deleted the quest/m1/moxygen/fetch branch September 27, 2026 04:16
kixelated added a commit that referenced this pull request Sep 29, 2026
quest(moxygen): block the line on the #4276 group fetch fill findings
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