Skip to content

fix(net): accept a clear FIRST_OBJECT that starts at object 0 (backport #5027) - #5069

Merged
kixelated merged 2 commits into
releasefrom
quest/m0/release-22/first-object
Oct 9, 2026
Merged

kixelated merged 2 commits into
releasefrom
quest/m0/release-22/first-object

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Problem

On release, moq-net and @moq/net drop every draft-18+ subgroup stream whose FIRST_OBJECT bit is clear, even when its first Object ID is 0. A publisher that leaves the bit clear on a whole group (out of spec on the bit, not missing a head) loses every group. #5027 fixed this on main.

Approach

Cherry-pick of fdfdf9a (#5027). A clear bit now peeks the first Object ID: 0 is delivered as the whole group; any other ID, or a stream with no object, is still dropped and the track resumes at the next group.

Adapted where release diverged:

Kept #5027's tests: clear bit at object 0 is delivered, clear bit at object 3 is dropped and the next group arrives. Both fail without the fix (checked in Rust and JS).

Impact

  • No public API change.
  • No wire change. Subscriber-side acceptance only: a draft-18+ stream with FIRST_OBJECT clear and first Object ID 0 is now delivered instead of dropped.

Alternatives

None; this is a straight backport.

Follow-ups

  • The quest file quest/m0/release-22/first-object.md lives on main and must be deleted there once this lands (it is not on release, so this PR cannot remove it).
  • just check origin/release passed apart from four moq-uring worker tests that fail locally on a shared RLIMIT_MEMLOCK limit, unrelated to this change.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 8, 2026 16:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backport of #5027 to release. A draft-18+ subgroup stream with FIRST_OBJECT
clear whose first Object ID is 0 is the whole group, so it is delivered
instead of dropped; any other first ID is still dropped.

Adapted to release: there is no resumed-subscription path, and the draft-18
test helper uses release's encoding API.

(cherry picked from commit fdfdf9a)

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

Copy link
Copy Markdown
Collaborator Author

Quest outcome: quest/m0/release-22/first-object.md is implemented by this backport. No open decisions; it stays a draft for the maintainer to mark ready. Once it lands, delete the quest file on main (it does not exist on release).

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 8, 2026 23:49

@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 23d91184d86a04c94460bf30cb3a6ecccd4da28d against release.

One P2 finding below: the new JS first-object peek outlives local unsubscribe when the publisher stalls. The object-zero compatibility fix and release-specific adaptations otherwise fit the intended scope; retain the narrow approach and add cancellation around the peek, as Rust already does.

Verification: GitHub-only static diff and surrounding-code review, including the upstream change. I did not run tests. GitHub's Check and Test jobs were still running when inspected.

return;
let id: bigint | undefined;
try {
id = await stream.peekU62();

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.

[P2] Cancel the first-object peek when the subscription closes

If a publisher sends a FIRST_OBJECT-clear subgroup header and stalls before completing its first Object ID, this new await stays pending after the last local subscriber closes. Subscription teardown closes the track/request stream and retires the alias, but does not stop this already-dispatched uni Reader; connection.ts only stops it after handleGroup settles. No group producer exists yet, so the later producer.closed race cannot help. The Reader, handler and active Tail entry therefore survive until the peer advances/ends the stream or the session closes, whereas this branch previously stopped immediately. Race the peek against track.closed and stop/return when cancellation wins. Add a test with a stalled first-ID read, local unsubscribe, and the session kept open.

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.

Not fixing this in the backport. The gap is real but it is not new to this change, and it is not specific to release:

  • main (fix(net): accept a clear FIRST_OBJECT that starts at object 0 #5027) has the same unraced peekU62().
  • With FIRST_OBJECT set, the first stream.decodeMaybe(decode) also runs unraced, because producer is still undefined until the first object arrives. So a publisher that stalls before its first object already holds the Reader, the handler, and the Tail entry after a local unsubscribe, until the peer moves or the session closes. The peek has the same lifetime as that existing path.

The fix (race both pre-producer awaits against the subscription going away, as the Rust recv_group already does) belongs on main first and then gets backported, so release does not diverge. I'll record it as a follow-up.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary for 23d91184d86a04c94460bf30cb3a6ecccd4da28d:

  • Backport of fix(net): accept a clear FIRST_OBJECT that starts at object 0 #5027 onto release, Rust and JS. A draft-18+ subgroup stream with FIRST_OBJECT clear is delivered when its first Object ID is 0, and dropped otherwise.
  • No public API or wire change.
  • CI: all checks pass.
  • Review: the OpenAI review of this head left one P2 (the JS peek is not raced against unsubscribe). Not fixed here: main has the same code, and the pre-existing unraced first decodeMaybe on the bit-set path has the same lifetime. It goes to main first as a follow-up, then a backport.
  • The quest file lives on main and is removed there in a separate PR after this lands.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 5d9d64a into release Oct 9, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m0/release-22/first-object branch October 9, 2026 00:01
@moq-bot moq-bot Bot mentioned this pull request Oct 9, 2026
kixelated added a commit that referenced this pull request Oct 9, 2026
Conflicts in js/net/src/ietf/subscriber.ts, js/net/src/ietf/subscriber.test.ts, and rs/moq-net/src/ietf/subscriber.rs resolve to main: release's side is the #5069 backport of #5027, which main already carries in full, including the resume.is_none() guard the backport dropped and the newer Encoder and moq_net_sim test harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moq-bot moq-bot Bot mentioned this pull request Oct 11, 2026
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