Skip to content

fix(net): keep a replaced route copy while a group it may carry is needed - #6141

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/resume-in-flight
Oct 21, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/resume-in-flight

Conversation

@kixelated

@kixelated kixelated commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Completes the same-epoch handover from moq-dev/moq#5836: a replaced route copy is let go only once nothing it may still carry is needed, in Rust (rs/moq-net/src/model/resume.rs) and JS (js/net/src/resume.ts) alike. Closes the quest quest/m1/resume-in-flight.md.

What changed

One retention rule for a replaced copy, judged from since: when a newer group superseded everything under the copy's bound (the new copy's first group, or the track's end). The copy stays while:

  • It may still deliver a group under its bound the reader lacks. An unread group, or one that may still land by the rule the track's own arrivals apply to a passed group (not accounted for, not dropped, within the feed, in a gap still within the grace). Neither is waited for past the reader's budget after since, so an arrival-order reader now gets F-1 when the old route was still sending it as the new route handed out F. An ordered reader past F counts F-1 delivered already, so it keeps nothing.
  • It backs a group still read. It holds that group, or may still get it within the grace, including as its first group when the group is at or past where the copy was asked to start. This replaces the hand-out-time backups leases with a check at let-go time against the groups still out, so a copy that gets F after hand-out backs it the same way as one that held it then.

Built on that:

  • Clean end. When the serving copy drains cleanly, the copies it replaced that were never bounded get bounded at its end. The end then waits while any replaced copy may still deliver below it, so a group the old copy still carries is no longer cut off.
  • Datagrams keep no copy. Datagrams are ephemeral, so only groups (unread, landing, or backing one still read) justify keeping a replaced copy. A reader that never asked for a group keeps no replaced copy after a swap, whatever its budget, so a route that keeps moving leaves no copy per swap behind. On main such a reader kept every replaced copy holding an unread group, one per swap. Candidate selection also skips a replaced copy past the reader's budget.
  • JS datagram reads. Subscriber.#tryDatagram always passes delivered, and Reader tracks whether groups were asked for (as Rust does), so a recvDatagram() after a swap no longer drops a replaced copy's unread group the group reader still needs.

Helpers: Ledger::gaps, Delivered::covers, plus track::Consumer::{may_land, now, poll_wall} in Rust and the hooks.mayLand hook in JS (TrackState::may_land / Producer.#mayLand).

Public API / wire

Behavior only; no exported API changes. Wire: none.

Decision

What a datagram-only reader keeps. The maintainer decided not to cache for datagrams, since they are meant to be ephemeral. A reader that only reads datagrams keeps no replaced copy at all, whatever its budget, including an unbounded one. Only groups justify retention. This drops main's behavior of bounding such a reader's replaced copies at what they hold for a later first group read. A reader that reads both channels asks for groups up front, so it is unaffected.

Tests

Regression tests, each failing on main and passing here (Rust under mock time, JS with Manual):

  • Rust resume.rs: an_arrival_reader_gets_a_group_still_in_flight_on_the_replaced_route (lands / dropped / expired), an_ordered_reader_keeps_no_copy_for_a_group_it_passed, a_group_the_replaced_copy_gets_late_backs_the_serving_copys, a_replaced_copy_backs_a_group_before_its_first_arrival, a_clean_end_waits_for_a_group_the_replaced_copy_still_carries, datagram_readers_keep_no_replaced_copy (bounded and unbounded budget); plus Ledger::gaps and Delivered::covers unit tests.
  • JS resume.test.ts: the same six scenarios, plus a port of the Rust datagram_reads_keep_unread_groups_on_a_replaced_copy.
  • Updated for the datagram decision: the Rust datagram_reads_keep_unread_groups_on_a_replaced_copy, datagram_retention_preserves_ordered_backlog and datagram_retention_obeys_changed_group_bounds now ask for groups before the swap, and datagram_readers_bound_replaced_copies is gone with the behavior it covered.

Checks: just check passes except the known load flake export::redundant_importers_switch_through_bufferbloat_on_lite_07, which passes 5/5 in isolation; the full Rust workspace suite passes (9245 tests), and so does bun test in js/net.

Follow-ups

  • Flapping between the same two routes creates one copy per swap of the same track. They are redundant as backups (track-level peeks), though each has its own subscription window. Deduping them for the backs rule would cap retention further; this was already true on main with hand-out leases.
  • The window filter (Window) is not applied to the may-land check, so a reader with sparse ranges can keep a replaced copy up to its budget for a gap outside its ranges.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 4 commits October 20, 2026 18:37
…eded

After a same-epoch route swap, a replaced copy now stays while it may
still deliver a group under its bound the reader lacks (unread, or still
landing within the grace), or back a group still read (held now or
landing later), for no longer than the reader's budget after a newer
group superseded it. A clean end of the serving copy waits for what a
replaced copy still carries below that end, a datagram-only reader keeps
replaced copies only within its budget, and a JS datagram read before
the first group read keeps the old copy's unread group. Rust and JS
alike.

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

# Conflicts:
#	rs/moq-net/src/model/resume.rs
Datagrams are ephemeral, so only groups justify keeping a replaced copy.
A reader that never asked for a group keeps none after a swap, whatever
its budget, instead of bounding each at what it holds for a later first
group read. Rust and JS alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 21, 2026 03:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 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-21T04:39:28.030582Z 8f44b0f Manual request
ℹ️ 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: cb5e009e67

ℹ️ 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 thread rs/moq-net/src/model/resume.rs Outdated
Comment on lines +445 to +448
|| (lands
&& self
.track
.may_land(sequence..sequence.saturating_add(1), false, grace, None))

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 Retain a copy before its first arrival

When a replaced copy has not received any group yet, this backup check calls may_land(..., false, ...); TrackState::may_land then returns false because arrivals.first() is None. Consequently, after a newer route hands out the first group, the reader immediately drops the older subscription even though that same group can still arrive there within the grace period. If the newer copy subsequently fails, recovery loses the viable late backup; the same issue lets a clean end cut off an in-flight first group. The mirrored JS path has the same behavior. Treat a never-started subscribed feed as capable of delivering its first group until its grace expires.

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

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, fixed in 8f44b0f. A replaced copy now backs a group at or past where it was asked to start even before its first arrival, while it may still land within the grace. Without a floor it still counts only what follows its first arrival, since nothing says where its feed starts. Same in JS. Regression test a_replaced_copy_backs_a_group_before_its_first_arrival (and its JS port) fails without the fix.

The clean-end case already counted from the floor, so it needed no change.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9d17e81a-9cd0-4088-bf48-e56a3a171667
📥 Commits

Reviewing files that changed from the base of the PR and between bc47380 and cb5e009.

📒 Files selected for processing: 15
  • js/net/src/delivered.test.ts
  • js/net/src/delivered.ts
  • js/net/src/internal.ts
  • js/net/src/ledger.ts
  • js/net/src/resume.test.ts
  • js/net/src/resume.ts
  • js/net/src/track.ts
  • quest/m1/README.md
  • quest/m1/resume-datagrams.md
  • quest/m1/resume-in-flight.md
  • quest/m1/resume-outage.md
  • rs/moq-net/src/model/delivered.rs
  • rs/moq-net/src/model/ledger.rs
  • rs/moq-net/src/model/resume.rs
  • rs/moq-net/src/model/track.rs
💤 Files with no reviewable changes: 4
  • quest/m1/README.md
  • quest/m1/resume-outage.md
  • quest/m1/resume-datagrams.md
  • quest/m1/resume-in-flight.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

The change adds delivered-range coverage, ledger gap queries, and live-feed landing checks in JavaScript and Rust. Both resume implementations update how they retain replaced route copies based on group demand, possible late arrivals, active reads, and reader budgets. New tests cover late groups, datagram reads, route swaps, and clean termination. Related quest documentation and links were removed.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to cb5e0

This change lets readers that switch to a new route keep the old route's copy only while it may still deliver groups they need, within each reader's delay budget. Datagram-only readers no longer hold old copies. No concrete defect has been identified. Readers with a very long or unbounded delay budget may see clean stream endings wait while an old route could still deliver a missing group, which matches the documented budget behavior.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: retaining a replaced route copy while a needed group may still be delivered.
Docstring Coverage Passed Docstring coverage is 89.47% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 10 files. (1 skipped: 1…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly describes the replaced-route-copy retention changes, Rust and JavaScript implementations, datagram behavior, tests, and quest documentation updates.
Full details: Docstring Coverage

Explanation

Docstring coverage is 89.47% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 10 files. (1 skipped: 1 too large.)

✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • 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.

A replaced copy that got no group yet backs one at or past where it was
asked to start, while that group may still land within the grace.
Rust and JS alike.

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

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 8f44b0fbe9

ℹ️ 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".

@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Queuing 8f44b0fbe93e41a75e8e68d186dc67b8f312318f.

  • A replaced route copy is kept only while a group it may carry is needed: one the reader lacks under its bound (unread, or still landing within the budget), or one backing a group still read. This holds in Rust and JS alike.
  • Maintainer decision: datagrams are ephemeral, so a reader that only reads datagrams keeps no replaced copy, whatever its budget.
  • Codex's finding on cb5e009e67 is fixed: a copy now backs a group before its first arrival. Codex's review of this head found no issues.
  • Public API: none. Wire: none.

(Written by Claude Opus 5.5)

Merged via the queue into main with commit 9b1b81a Oct 21, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m1/resume-in-flight branch October 21, 2026 10:10
kixelated added a commit that referenced this pull request Oct 21, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 21, 2026
Resolve js/net resume conflicts with #6141: the clean end now concludes
only once no group the reader lacks may still land on a replaced copy,
on every path (group drain, datagram drain, the serving copy closing).
A replaced copy's unread group keeps it without delaying closed.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant