Skip to content

fix(net): an aborted live copy doesn't hold a track's end open - #7149

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/settled-abort
Oct 29, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/settled-abort

Conversation

@kixelated

@kixelated kixelated commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

TrackState::is_settled (rs/moq-net/src/model/track.rs) decides whether a track's declared end has settled, so that a later abort keeps the clean end. It checked only each slot's live copy, slot.group. Suppose the live copy of a group was cut or evicted while a fetched head still served that group to its end. When the head finished, the end stayed unsettled, and a later track abort turned the clean end into an error. poll_outcome had the same gap: it waited on slot.group.poll_closed and treated an aborted live copy as never closing.

Found while landing moq-dev/moq#7053.

Approach

  • New Slot::is_settled: the copy serving the slot (Slot::serving(), the same choice serves_end and cut() make) finished. A finished copy counts even if a transport bound cut it short.
    • If a fetched head serves the group to its end, it is judged instead of the live copy beside it, whether that live copy is cut, evicted, or still being written.
    • Once no head serves, the live copy is judged as before. An aborted live copy still holds the end open.
  • TrackState::is_settled now uses Slot::is_settled.
  • poll_outcome now waits on the same predicate. It registers on every copy, because a serving head that aborts hands the slot back to the live group, and then judges again, so a copy that closes in the meantime is not missed.
  • js/net is unchanged. Its track cache holds one copy per group with no fetched head, so the head case cannot happen there. Its aborted-group rule already matches Rust's.

Tests (in track.rs). Both head tests fail on main:

  • a_fetched_head_settles_the_end_once_it_finishes: the live copy is cut (Cancel), evicted, or finished. The end settles when the head finishes, and a later abort ends cleanly. Covered both with and without closed() seeing the end first.
  • a_finished_head_settles_the_end_over_an_open_live_copy: the end settles while the live copy is still being written, and that copy aborting later keeps the end clean.
  • an_aborted_head_hands_the_end_back_to_the_live_copy: the head aborts. A finished live copy settles the end. A cut one does not, so the track abort still errors.

Choices made without asking

  • Narrower than the quest's literal wording. The quest recommended that "an aborted live copy counts as nothing in flight". My first cut treated any aborted serving copy as settled. That broke subscription_end_integrity::a_subscription_cut_by_the_session_close_does_not_end_clean: when a session dies mid-group, it aborts the live copy, and the subscription then ended Ok(None) short of the final group. The moq-lite rule is that a subscription ends cleanly only once every group is accounted for. So an aborted copy counts as nothing in flight only when another copy (the fetched head) serves the group's end.
  • A fetched head that is still writing now holds the end open, even if the live copy finished. Before this change, a finished live copy settled the end while a head that readers were served from was still in flight, and a track abort would then fail that head's readers under a clean outcome. This matches a_fetched_head_holds_the_largest_open. An abandoned fetch aborts its head, which hands the slot back to the finished live copy, so this cannot hang.

Impact

  • Public API: none.
  • Wire: none.

Suggested follow-ups

  • An aborted invisible backfill slot (a cancelled fetch below the end) still holds the end open until the expiry scan reclaims it. That behaviour is unchanged here and timing-dependent. Worth a look alongside Drop set.
  • DropCursor::cut still reports a cut that a fetched head covers. This was raised in moq-dev/moq#7053 and is not addressed here.

Validation

  • just check origin/main: pass on the branch before merging main (9023 Rust tests, plus clippy, fmt, quest, and shell checks).
  • cargo test -p moq-net --all-features after merging main: 2649 lib tests and 491 integration tests pass.
  • quest check: pass. The finished quest and its link in quest/m1/README.md are removed.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 4 commits October 28, 2026 15:04
Quest-Claim: dfc62673f9de2a77
A track's declared end now judges each slot below it by the copy serving
its end: a fetched head that finished settles the end over a live copy
that was cut or evicted, and an aborted copy counts as nothing in flight.
A later abort then keeps the clean end instead of turning it into an
error. The outcome poll watches every copy, so a serving head aborting
hands the wait back to the live group.

js/net mirrors the aborted-copy rule; it caches one copy per group, so
it has no fetched-head case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Treating every aborted copy as settled let a session that died
mid-group end its subscription cleanly, which the subscription
integrity test rejects. Narrow the rule to the copy serving the slot:
a fetched head serving the group to its end settles it over a cut,
evicted, or still-written live copy, and an aborted live copy with no
head left still holds the end open. js/net caches one copy per group,
so it keeps its check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title An aborted live copy doesn't hold a track's end open fix(net): an aborted live copy doesn't hold a track's end open Oct 28, 2026
@kixelated
kixelated marked this pull request as ready for review October 28, 2026 23:49
@coderabbitai

coderabbitai Bot commented Oct 28, 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 26 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: 9c09b071-fd60-4e52-be6e-a71dd129003d
Commits

Reviewing files that changed from the base of the PR and between 97387af and b1a1be0.

Files selected for processing: 3
  • quest/m1/README.md
  • quest/m1/settled-abort.md
  • rs/moq-net/src/model/track.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 28, 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-29T05:02:11.886706Z b1a1be0 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.

@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: 0a36262

No actionable bugs found. Judging settlement through the slot's existing serving-copy selection is a focused fix: a completed fetched head can settle the end without waiting on the abandoned live copy, while an aborted head falls back to the live copy. Watching both copies before rechecking settlement also preserves wakeups across that handoff. The added tests cover live cancellation/eviction, a still-open live copy, head abortion, and clean-outcome latching with and without a preceding poll; no API or wire expansion is needed.

Verification: static review of the diff and surrounding track/group settlement and polling code only. I did not run the tests or exercise concurrent transport delivery.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary:

  • Merged main into the branch (b1a1be0). The only conflict was the quest/m1/README.md link list; it kept main's new entries and this PR's removal of the settled-abort link. rs/moq-net/src/model/track.rs merged cleanly.
  • just ci origin/main passed locally on the merged head (8973 tests, plus clippy, fmt, quest, and shell checks).
  • Codex reviewed the new head with no findings, and the earlier automated review of 0a36262 had none either. No review threads are open.
  • quest check --base origin/main --branch quest/m1/settled-abort passes. The finished quest and its link are deleted.
  • No design changes or open decisions. Landing through the merge queue.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit dffb2a4 Oct 29, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/settled-abort branch October 29, 2026 07:21
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