Skip to content

fix(moq-net): a recovered run leaves the copy's subscription - #8700

Merged
kixelated merged 2 commits into
mainfrom
quest/m2/subscribe-ranges/recovery-duplicate
Nov 14, 2026
Merged

kixelated merged 2 commits into
mainfrom
quest/m2/subscribe-ranges/recovery-duplicate

Conversation

@kixelated

@kixelated kixelated commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Completes A recovered run is served once (deleted here).

Summary

A lite-07 copy keeps an asked run whole in its subscription while any fill still owes part of it. A stale drop of part of that run opens a Recovery for that part, but the part stayed in the copy's ranges. On lite-07 every SUBSCRIBE_UPDATE reopens each stale drop still in range. So an update that loosens the budget (for example a new live reader with a wider budget) let the publisher re-offer the recovered groups on the demand subscription while the recovery also served them. The copy then reset the duplicate stream.

Fix, at the root and the same in Rust (RangeFills::poll) and JS (RangeFills.poll): whatever a recovery still covers is removed from the ask, even out of a run otherwise kept whole. The update that narrows the ranges goes out as the recovery opens. The recovered groups were dropped on that subscription, so no stream arriving there is cut.

Decisions (taken by the implementer, for review)

  • Picked the Plan's first option, "the recovery's run stays out of what the update reopens", over settling which subscription delivers a group. It is a 3-line change per language, needs no wire signal, and keeps both settled Lite-07 FETCH is single-group only #8616 decisions (no max delay on a recovery, and a narrow reader may take a recovered run's stale groups).
  • It costs one SUBSCRIBE_UPDATE when a recovery opens on part of a run, which the old "the subscription does not change under the recovery" rule avoided. That rule is what kept the duplicate possible, so the JS test asserting it now asserts the carve-out.
  • The Plan asked for a two-hop reproduction. An end-to-end two-hop setup cannot stage a partial stale drop: the copy's demand budget widens to its most tolerant reader, so the reader whose fill rides the subscription is never judged stale there (tried in range_fill.rs, where it passed without the fix). Instead, the reproduction drives the copy's real RangeFills with the subscriber's lite-07 harness and asserts the precondition of the duplicate: the recovered run is gone from what the next update carries. The publisher side (an update reopening a stale drop still in range) is already covered by an_update_reopens_every_stale_drop_and_supersedes_the_rest.

Impact

  • Public API: none. Wire: none (a copy now sends one SUBSCRIBE_UPDATE earlier).

Caused by

Validation

  • Rust lite::subscriber::tests::a_recovered_run_leaves_the_subscription: fails with the fix reverted (ask stays [1..5]) and passes with it.
  • JS range fills: a stale drop of asked groups is recovered once, on a subscription of its own: fails with the fix reverted, passes with it. bun test src/lite/ in js/net: 866 pass, 0 fail.
  • just check (scoped to the branch) passed on e2cc4da.

Follow-ups

None.

Models

Size: S
Implementer: Claude Code, claude-opus-5-5, unknown effort

(Written by Opus 5.5)

Review

Rewritten by quest review after each round.

  • Head: e2cc4da

  • Base: origin/main at 98e54e7

  • Reviewer: Codex, gpt-6.1-sol, medium effort

  • Session: 01a1d790-fecb-7aa0-9e5e-721dbbc11ee8, round 1

  • Score: 9/10, The Rust and JS changes consistently exclude recovering groups from retained fill ranges, with no actionable regression identified by source inspection; builds and tests were not run as instructed.

  • Rounds: 1; findings 0: fixed 0, refuted 0, open 0

  • Findings: none

kixelated and others added 2 commits November 13, 2026 16:43
A run kept whole while a fill owed any of it kept its recovered part in the
copy's ranges. Any later SUBSCRIBE_UPDATE reopens every lite-07 stale drop
still in range, so a loosened budget let the publisher serve the recovered
groups on both the recovery and the subscription. Rust and JS now carve what
a recovery covers out of the ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A recovered run is served once fix(moq-net): a recovered run leaves the copy's subscription Nov 14, 2026
@kixelated
kixelated marked this pull request as ready for review November 14, 2026 01:00

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

I reviewed the Rust and JS change that takes groups under recovery out of a lite-07 copy's demand subscription, along with its tests and the deleted quest file.

  • Carve-out at the end of RangeFills::poll / RangeFills.poll: whatever recovering still covers is now removed from groups after the keep-whole and draining passes, so a later SUBSCRIBE_UPDATE can no longer reopen the stale drop while the recovery serves the same groups. The Rust and JS versions match: RangeSet::remove in Rust and a removeRun fold in JS.
  • Ordering is sound: the carve-out runs after draining and reading are computed, so a draining run that overlaps a recovery still tracks its in-flight streams. Because recovering is rebuilt by cover() minus released, groups that were handed back are not kept out by mistake, and refused groups stay out through waiting.
  • Tests: the new Rust a_recovered_run_leaves_the_subscription checks the ask shrinks from [1..5] to [3..5] once 1..3 is recovering. The JS assertion in fills.test.ts now expects the split [0..1], [3..4] instead of the whole run.
  • Known side effect: splitting an asked run makes retire_skipped drop that run's skipped entry. At worst this costs one more re-ask of a skipped start, which is consistent with the PR's stated cost of one extra update.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@coderabbitai

coderabbitai Bot commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

JavaScript and Rust RangeFills.poll now exclude actively recovering groups from subscription asks. Tests check that a partial recovery leaves the remaining groups requested and that recovery retains the fill’s priority. The recovery design note was deleted.

Estimated code review effort: Simple

Merge Risk: Minimal · up to e2cc4

This change stops groups that a recovery is already serving from being re-requested through the copy's subscription, which reduces duplicate delivery. It adds no API or wire-format change. A narrow, pre-existing overlap through demand ranges remains, and it is not made worse by this change. The merge risk is minimal.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the change: a recovered run is removed from the copy's subscription, which matches the RangeFills.poll changes in Rust and JS and the stale-drop recovery tests. It is specific and foll…
Description check Passed The description explains the cause (a lite-07 copy keeps an asked run whole while a fill owes part of it), the fix in Rust and JS RangeFills.poll, the decisions taken, the reproduction approach, and t…
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.
Full details: Title check

Explanation

The title names the change: a recovered run is removed from the copy's subscription, which matches the RangeFills.poll changes in Rust and JS and the stale-drop recovery tests. It is specific and follows the conventional-commit style. The title is short, but it does not name the duplicate-delivery symptom.

Full details: Description check

Explanation

The description explains the cause (a lite-07 copy keeps an asked run whole while a fill owes part of it), the fix in Rust and JS RangeFills.poll, the decisions taken, the reproduction approach, and the validation results. It relates directly to the changeset.

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

Recent review info

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: b6c3988f-8ce3-4b7a-ad5c-0591e2ec524e
Commits

Reviewing files that changed from the base of the PR and between 5e56ba4 and e2cc4da.

Files selected for processing: 5

  • js/net/src/lite/fills.test.ts
  • js/net/src/lite/fills.ts
  • quest/m2/subscribe-ranges/recovery-duplicate.md
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/lite/subscriber/fills.rs
Files with no reviewable changes: 1

  • quest/m2/subscribe-ranges/recovery-duplicate.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.

@kixelated
kixelated added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 58e34dc Nov 14, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m2/subscribe-ranges/recovery-duplicate branch November 14, 2026 03:02
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