Skip to content

fix(net): read SUBSCRIBE FINs that raced a peer's close - #6741

Merged
kixelated merged 2 commits into
mainfrom
quest/m1/broadcast-lifecycle/lite-close-fin
Oct 25, 2026
Merged

kixelated merged 2 commits into
mainfrom
quest/m1/broadcast-lifecycle/lite-close-fin

Conversation

@kixelated

@kixelated kixelated commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

On lite-03/04 a subscription's FIN is the track's only end (no SUBSCRIBE_END). A publisher's graceful close drains on the transport's acknowledgement of that FIN, so the close can reach a relay before the relay reads the FIN. The relay's subscriber half then never polled the subscription again: its publisher half failed on the closed accept first, the drain read only group streams, and cleanup aborted every track. Downstream, a finished track read as Unroutable instead of finished. Found by relay_upstream_close in moq-dev/moq#6660.

Approach

Fix the receiver's close ordering, with no extra wait or timer:

  • SubscriberDriver::poll_drain: once the closed transport's group streams drain, poll the sources once, so each subscription reads what the peer already delivered. A buffered FIN takes the existing Tail path, which finishes the track on a closed session. A subscription without one hands the track back as before. Serve machines still wait for the group streams, since ending a subscription removes the state the tails are delivered into.
  • TrackServeRun: a closed session's auth grants end with it, which no longer counts as a revocation. Without this the final poll aborted tracks with Unauthorized (caught by source_errors::track_in_flight_through_a_dead_upstream_is_unroutable).
  • relay_upstream_close now finishes three tracks together so several SUBSCRIBE FINs are pending at the close, and runs Close on lite-03/04 too.

Choices made without asking:

  • Fixed on the receiver, not the publisher. Making a lite-03/04 publisher wait for the subscriber's FIN (as lite-07 does) would be an extra wait, and older lite-03/04 peers are not required to send one.
  • The extra poll applies on every lite version. It also covers lite-05/06, where SUBSCRIBE_END and the FIN can race the close the same way.

Impact

  • No public API or wire change.
  • Behavior: when a lite peer closes its session, subscriptions whose FIN the transport already delivered now finish instead of aborting.

Alternatives

  • Publisher waits for the subscriber's FIN on lite-03/04: rejected, see above.
  • Polling the sources during the group-stream drain: rejected, since it ends subscriptions before their tails land.

Follow-ups

  • js/net may have the same ordering on a peer's close; not checked here.

Checks

  • just check: passed.
  • Reproduced first: lite-03 Close failed with Err(Stream(Unroutable)) and no frames before the fix.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 24, 2026 23:33
On lite-03/04 the subscription FIN is a track's only end, and the
publisher's graceful close drains on its transport acknowledgement, so the
close can reach the relay before the relay reads the FIN. The subscriber
half then aborted the track instead of finishing it.

After the closed transport's group streams drain, give each source one
last poll so a subscription reads what the peer already delivered. A
closed session's grants end with it, which is not a revocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title lite-03/04 deliver SUBSCRIBE FIN before a graceful close fix(net): read SUBSCRIBE FINs that raced a peer's close Oct 25, 2026
@kixelated
kixelated marked this pull request as ready for review October 25, 2026 07:13
@coderabbitai

coderabbitai Bot commented Oct 25, 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 3 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
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1a9a77e5-8228-421b-b0e8-10ace17aeae3
Commits

Reviewing files that changed from the base of the PR and between 1b4b61e and aa032c8.

Files selected for processing: 4
  • quest/m1/broadcast-lifecycle/README.md
  • quest/m1/broadcast-lifecycle/lite-close-fin.md
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/tests/integration/relay_upstream_close.rs
  • Autofix · 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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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-10-25T07:15:19.899356Z aa032c8 Draft marked ready
ℹ️ 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.

@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: aa032c8

No actionable bugs found. Draining received group streams before polling subscription completions preserves their destination state, and letting the closed session determine track termination avoids misclassifying teardown as authorization revocation. This receiver-side ordering fix is simpler than adding a publisher timeout or a new FIN handshake for older peers. The expanded three-track regression covers simultaneous completions across close and abort paths.

Verification: static inspection of the complete patch, relevant lifecycle/auth/task code and regressions. Tests and real-transport interoperability were not run.

@kixelated
kixelated added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 5b5fcf0 Oct 25, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/broadcast-lifecycle/lite-close-fin branch October 25, 2026 16:46
kixelated added a commit that referenced this pull request Oct 25, 2026
Its last children landed here and in #6741, and its goal shipped in
#6660, #6656, and #6649.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 25, 2026
Since #6741 a closed lite session drains its sources in a fixed order, so
the catalog failure always reaches the cursor first and the loop waiting
for both orders could never finish. Each disconnect now runs once and
accepts any typed failure; export unit tests cover the timeline failure.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant