Skip to content

fix(net): refuse new requests from retired sources - #5294

Merged
kixelated merged 3 commits into
mainfrom
quest/m0/broadcast-epoch/retired-requests
Oct 11, 2026
Merged

kixelated merged 3 commits into
mainfrom
quest/m0/broadcast-epoch/retired-requests

Conversation

@kixelated

@kixelated kixelated commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A held broadcast or track could open a new epochless request after replacement and read the new publisher's content at the same path. Exact-path FETCH reproduced on Lite-05/06, epochless Lite-07 and IETF 14–22. A more-specific route could also redirect requests through a still-live covering source.

Approach

Carry private source validity through Rust Lite/IETF request setup and JS consumers. The existing route prefix index tracks which instance this peer would serve for the requested path. Losing selection aborts the captured request generation; a still-live fallback may receive a new generation when selected again, without reviving canceled work. Metadata, deferred subscription setup and standalone FETCH share that lifetime. Explicit epochs keep proving the held identity.

Accepted reads, joining FETCH and old cached bytes remain usable. Unanswered legacy IETF requests are canceled explicitly to release demand. Retirement cancels only the wait for a request ID: once an ID is assigned, its request is written and then canceled, in both Rust and JS, because drafts 14–16 grant MAX_REQUEST_ID credit back only for requests the peer receives. Lite SUBSCRIBE_START arrives with first data, so completed SUBSCRIBE writing is its establishment boundary; IETF remains gated through SUBSCRIBE_OK. The unavoidable ambiguity before an epochless peer observes replacement remains.

Impact

  • Public API: corrected request lifetime/refusal behavior, no new supported exported shapes. A hidden fuzz/benchmark hook measures the private Rust gate; the internal JS ID allocator accepts an AbortSignal.
  • Wire: no framing, field, registry or negotiation change; existing refusal/cancellation messages are used.
  • Existing user documentation explains held handles. Rust retirement and JS cache fanout benchmarks run in the existing nightly lanes.

Iteration (2026-10-11)

Rebuilt on main after #5254 (squash-merged) and #5252, dropping the dependency commits; this PR no longer touches js/net/src/origin.ts. Decisions taken by the iterating agent, which could not prompt:

Confirmed by the maintainer at merge:

  • Codex P2, allocated ID: ✅ send the allocated request, then cancel it (send-then-cancel). Alternative: open the stream before allocating.
  • Codex P2, legacy TRACK_STATUS credit: ✅ declined in this PR, no follow-up quest. Alternative: keep the local request draining or add a protocol-visible retirement.

Validation

  • Regression a_retirement_after_allocation_still_sends_the_request (mock time, draft-16, SUBSCRIBE / TRACK_STATUS / FETCH) fails before the Rust fix with no request on the wire and passes after.
  • Previous head: all 12 exact-path FETCH regressions fail on the base and pass with the fix; the covering-source regression fails the initial fix and passes the selection gate; JS orphan-allocator regressions pass with contiguous subsequent IDs.
  • nix develop --command just check passes on the rebuilt head (9698ea3).
  • just test interop --all: every non-browser round passes. Two full runs on a heavily loaded host (load ~45 on 24 cores) each timed out one different browser round (js -> c, then js -> js at 143s of 30s); a targeted --publishers js --subscribers c,js,rust rerun passes.

Fanout measurements sweep 8/32/128 routes by 1/8/32 affected consumers. Rust route-only control costs 1.2–3.2 µs but misses every covering replacement; selection costs 2.8–20.7 µs and revokes every affected lease. JS baseline costs 0.17–0.27 µs but cancels no requests; the fix delivers every expected notification, with repeat means of 1.7–33.9 µs. These measure correctness overhead, not an optimization claim; work visits indexed affected paths.

Alternatives

Closing old subscriptions would discard valid reads. Requiring epochs would remove required legacy support. Path-wide invalidation would retire unrelated consumers. Selection generations preserve the held instance and cancel pending work without changing published framing.

Follow-ups

Completes quest/m0/broadcast-epoch/retired-requests.md; its remaining references point here. #5314 (JS Origin request/follow refactor) builds on this.

(Written by GPT-6 Astra; iterated by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 11, 2026 01:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 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-11T18:40:17.890779Z 9698ea3 New commits
🔒 Security Review ✅ Completed 2026-10-11T01:24:40.175989Z ef59a7b 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.

@coderabbitai

coderabbitai Bot commented Oct 11, 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: d880eb06-6c53-4b65-a54e-7597d02e3949




📥 Commits

Reviewing files that changed from the base of the PR and between ef59a7b and 9698ea3.





📒 Files selected for processing (13)
  • js/net/bench/broadcasts.ts
  • js/net/src/consume.test.ts
  • js/net/src/consume.ts
  • js/net/src/ietf/adapter.test.ts
  • js/net/src/ietf/subscriber.test.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/lite/subscriber.test.ts
  • js/net/src/lite/subscriber.ts
  • quest/m0/broadcast-epoch/README.md
  • quest/m0/broadcast-epoch/catalog-references.md
  • quest/m0/broadcast-epoch/retired-requests.md
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/model/origin.rs




💤 Files with no reviewable changes (2)
  • quest/m0/broadcast-epoch/README.md
  • quest/m0/broadcast-epoch/retired-requests.md




🚧 Files skipped from review as they are similar to previous changes (1)
  • quest/m0/broadcast-epoch/catalog-references.md




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






Walkthrough

The change adds selection-validity tracking to JavaScript broadcast caches and Rust route tables. Subscriber operations use that validity to stop pending requests when a selected source retires. Tests cover route changes, request-ID allocation, retirement during setup, and preservation of existing reads. New benchmarks measure route withdrawal and retirement behavior. Documentation and quest references are also updated.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 9698e

No identified defect remains that needs correction before merge; proceed with normal checks.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 71.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 71 functions across 18 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check Passed The title clearly and concisely describes the main change: preventing new requests from retired sources.
Description check Passed The description directly explains the retirement problem, implementation approach, impact, validation, and follow-ups. It is fully related to the changeset.

Full details: Docstring Coverage

Explanation

Docstring coverage is 71.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 71 functions across 18 files. (1 skipped: 1 unsupported.)


  • Fix all pre-merge checks with AI
✨ 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.

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

ℹ️ 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/ietf/subscriber.rs Outdated
Comment on lines +2003 to +2005
let mut stream = match validity
.run(Stream::open(&mut self.session.clone(), self.version))
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve allocated IDs across validity cancellation

On flow-controlled drafts 14–16, if the source is invalidated after next_request_id returns but before this second validity check, the function exits without sending a request for the allocated ID. Control::next_request_id has already advanced the counter, while the peer only extends MAX_REQUEST_ID for requests it receives and retires, so repeated route churn can permanently consume the request window and make later subscriptions time out despite having no live requests. Open before allocating, or ensure that every allocated legacy ID is sent and then canceled; the same allocation-before-cancellable-open pattern also appears in track_status and run_group_fetch.

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 and fixed in 9698ea3. Retirement now cancels only the wait for an ID (validity.run is atomic with allocation). Once next_request_id returns, subscribe_once, track_status and run_group_fetch open and write the request unconditionally, and the response wait cancels it with UNSUBSCRIBE / FETCH_CANCEL. Regression: a_retirement_after_allocation_still_sends_the_request parks the write, retires the source, then releases it; it put nothing on the wire before the fix.

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 11, 2026 10:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drafts 14-16 grant MAX_REQUEST_ID credit back only for requests the peer
receives. Retirement now cancels only the wait for an ID; an allocated
SUBSCRIBE, TRACK_STATUS or FETCH is always written, then canceled by the
response wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now cancels a lite fetch setup when its group closes. Both signals end
the setup, and the retirement tests use main's stream-open helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the quest/m0/broadcast-epoch/retired-requests branch from ef59a7b to 9698ea3 Compare October 11, 2026 18:37

@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: 9698ea3aa2

ℹ️ 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 on lines +2841 to +2843
if let Err(err) = validity.check(waiter) {
return Poll::Ready(Some(Err(err)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep legacy TRACK_STATUS alive until the peer retires it

On drafts 14–16, if the source retires while an unanswered TRACK_STATUS is blocked in the publisher's track.query(), this branch ends the local request even though the virtual request stream has no wire-visible cancellation: VirtualSendStream::finish and reset are no-ops. Even after the allocation fix, the remote handler can therefore remain pending and keep its RequestPermit, so repeated route replacements can exhaust the peer's MAX_REQUEST_ID window and block later requests. For these legacy drafts, continue draining the response or add an explicit protocol-visible retirement path rather than treating validity loss as a completed response.

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.

Declining in this PR. Drafts 14-16 have no TRACK_STATUS cancellation, so the peer's RequestPermit retires only when its handler answers. Nothing the subscriber does locally changes that: draining the response would not return credit any sooner, and a late answer is already dropped harmlessly by Shared::push once the virtual stream is gone. A local request abandoned by demand (reject_unused) has had the same exposure since before this PR. Retirement just adds one more local terminal.

The underlying risk is a legacy peer holding a TRACK_STATUS open indefinitely. The fix for that belongs on the answering side, which would bound the wait or refuse, and not in this request-lifetime change. I'll suggest it as a follow-up.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary

Held broadcast and track handles can no longer open new epochless requests after their source is retired. The Rust Lite/IETF subscribers and the JS consumers carry a private source-validity signal, captured at each selection. Accepted reads, joining FETCH and cached bytes stay usable. Pending setup is canceled on the wire. The branch was rebuilt on main after #5254 and #5252, so it no longer touches js/net/src/origin.ts.

Decisions:

  • Codex P2, allocated ID: ✅ send-then-cancel. Retirement cancels only the wait for an ID; once one is allocated, SUBSCRIBE, TRACK_STATUS and FETCH are always written and then canceled, so drafts 14-16 never lose MAX_REQUEST_ID credit. Regression: a_retirement_after_allocation_still_sends_the_request.
  • Codex P2, legacy TRACK_STATUS credit: ✅ declined, no follow-up quest. Drafts 14-16 cannot cancel a TRACK_STATUS on the wire, and abandoning one already had the same exposure.

Verified on 9698ea3aa2b5b08c2d2c9b0a497f2d269a19238b: all CI checks pass, CodeRabbit (no actionable comments) and Codex reviewed this head, and local just check passes. Public API: request lifetime and refusal behavior are corrected, with no new exported shapes. Wire: unchanged.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit f0fcbef Oct 11, 2026
13 checks passed
@kixelated
kixelated deleted the quest/m0/broadcast-epoch/retired-requests branch October 11, 2026 19:47
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