Repository navigation
fix(moq-net): follow a GOAWAY redirect from a draining fallback session - #8737
Merged
Merged
Conversation
Quest-Claim: 613be5f867589eb8
When a probe of the preferred transport gets through, the fallback session drains as the predecessor. Its GOAWAY watcher now moves with it, and while no replacement serves, an accepted redirect becomes the only target and drops a dial still headed for the leaving node. js/net's Reload mirrors it. The reconnect model gains FollowPred; RedirectFollowed moves into Handover.cfg and RestartWait.cfg, and RedirectLost.cfg is deleted. Also rewords the Backoff doc to what the redirect cycle check does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
marked this pull request as ready for review
November 14, 2026 14:45
Contributor
|
Warning Review limit reachedYou'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 27 minutes. View limit details
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. |
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed the fix for a GOAWAY redirect that was lost after a probe moved the client off its WebSocket fallback. I traced it through Reconnect::poll, the moq-tokio driver, and the JS Reload run loop.
- Rust
follow:probednow moves the fallback'sgoaway::ConsumerintoPredecessor::redirect. While no replacement is live,pollacts on an accepted redirect. A dial still in flight is abandoned, a backoff in progress keeps pacing, and an empty, refused, or closed GOAWAY is dropped. The probe sends our own GOAWAY throughgoaway::Producer, which writes to separate state from the peer'sConsumer, so it never trips this path.moq-tokiohandles the newAction::Abandonby dropping the probe-turned-dial held indial. Thedial_failedflag thatAbandonsets has no effect, because the next dial resets it or sets it again beforeRetryingreads it. - JS
#followand#applyPending: the redirect is held while a replacement serves and applied once the fallback is the one serving again. If a dial is in flight,#tickreruns the effect, andestablishcloses the reusedreached.transportwhen that run aborts. During a backoff#dialingis false, so the wait is kept, as in Rust. - Model and docs:
FollowPredand the per-sessionwatchedflag model the change, andRedirectFollowedmoves intoHandover.cfgandRestartWait.cfg.RedirectLost.cfgand the quest file are removed. TheBackoffandAction::Abandondocs now describe what the code does.
claude-opus-5-5 | 𝕏
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
When a QUIC/WebTransport probe gets through, the fallback session drains as the predecessor. Before this, its GOAWAY watcher was dropped, so a redirect its peer sent while it drained was lost and the next dial went back to the leaving node. Now:
rs/moq-net/src/reconnect.rs):probedmoves the fallback'sgoaway::Consumer(with its URL) intoPredecessor::redirect.pollcalls a new privatefollowwhile no replacement serves. An accepted redirect becomes the only target (the configured targets are kept for the fallback walk, as inend), resets the walk, and drops a dial still headed for the leaving node withAction::Abandon. A backoff that is already running keeps pacing the redial. A restart in place, a refused redirect, or a closed session is ignored, because we are already leaving that session. Fixed targets get no watch.js/net/src/connection/reload.ts):Reloadgets a matching#follow. After leaving the fallback, it watches the drained session'sdraining. It holds a redirect while a replacement serves and applies it whenever the fallback is the one serving (restarting the run if a dial is in flight), dropping it once the fallback retires. This matches Rust, which polls the watcher only outsidePhase::Live.Handover.tlagainsFollowPredand a per-sessionwatchedflag.RedirectFollowednow lives inHandover.cfgandRestartWait.cfg, andRedirectLost.cfgis deleted. Theverify-reconnectskill is updated.Backoffdoc: reworded to match what theRedirectscycle check does. It catches a loop within a few laps, not at the first revisit (A→B, B→C, C→B is followed once). There is no code change.quest/m1/reconnect-probe-redirect.md.Public API: no change (
Action::Abandon's doc now also covers this case). Wire: no change.Decisions
Taken by the agent under /quest-afk, for the maintainer's review:
followdoes no redirect-loop scoring. Rejected: running it throughRedirects::departand backing off on a loop, asenddoes. A redirect only reaches this path after a probe got through, so the probe's backoff already paces it. The session the dial lands on is scored byendwhen it ends. This keeps Rust and JS simple and in step.end, because the policy is already leaving that session. Rejected: ending the policy asenddoes.Validation
verify-reconnect:Handover.cfg(4 sessions, 1,388,332 distinct states) andRestartWait.cfg(303,814 states) pass every rule, includingRedirectFollowed. Mutation check: whenProbeGetsThroughstops moving the watcher,RedirectFollowedis violated again.residual_a_redirect_after_a_probe_got_through_is_lostis flipped toa_redirect_after_a_probe_got_through_is_followed(the Abandon, then a Dial to the named node). It has a sibling,a_redirect_during_the_backoff_after_a_probe_is_followed.cargo test -p moq-net --features async reconnect::: 48 pass.cargo test -p moq-tokio connection: all pass.a redirect from the draining WebSocket fallback is followed, covering the case where the handshake over the probe fails, the GOAWAY arrives during the backoff, and the redial goes to:4444. It fails with#followdisabled and passes with it. Lite-06's handshake needs no server round trip, so the in-flight-dial case can't be staged in JS. The Rust test covers it.a redirect from the draining WebSocket fallback outlives a replacement that closes(from review round 1): the redirect is held while the probe's replacement serves, then followed once it closes. It fails with#applyPendingremoved from the close path.just checkpasses.Caused by
Follow-ups
None.
Models
Size: S
Implementer: Claude Code, claude-opus-5-5, unknown effort
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)
Review
Rewritten by
quest reviewafter each round.Head: dfeaaa8
Base: origin/main at bd617cc
Reviewer: Codex, gpt-6.1-sol, medium effort
Session:
01a1da7e-01c9-77e1-afbe-758276fd46dd, round 2Score: 10/10,
Both earlier findings are fixed, and source inspection of the incremental changes found no new landing blockers; no tests or builds were run.Rounds: 2; findings 2 (medium 1, low 1): fixed 2, refuted 0, open 0
Findings: none
The last round's findings, at 0754259:
js/net/src/connection/reload.ts:819:Retain the redirect while a replacement temporarily serves;The head retains the fallback's redirect in #pending and calls #applyPending after a replacement closes and the fallback resumes serving, so the next dial follows the redirect..claude/skills/verify-reconnect/SKILL.md:47:Advance the model's sync reference;The sync reference and accompanying git log command now both name bd617cc1ee, matching the requested synchronization base.