Skip to content

fix(moq-net): follow a GOAWAY redirect from a draining fallback session - #8737

Merged
kixelated merged 3 commits into
mainfrom
quest/m1/reconnect-probe-redirect
Nov 14, 2026
Merged

kixelated merged 3 commits into
mainfrom
quest/m1/reconnect-probe-redirect

Conversation

@kixelated

@kixelated kixelated commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • Rust (rs/moq-net/src/reconnect.rs): probed moves the fallback's goaway::Consumer (with its URL) into Predecessor::redirect. poll calls a new private follow while no replacement serves. An accepted redirect becomes the only target (the configured targets are kept for the fallback walk, as in end), resets the walk, and drops a dial still headed for the leaving node with Action::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 (js/net/src/connection/reload.ts): Reload gets a matching #follow. After leaving the fallback, it watches the drained session's draining. 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 outside Phase::Live.
  • Model: Handover.tla gains FollowPred and a per-session watched flag. RedirectFollowed now lives in Handover.cfg and RestartWait.cfg, and RedirectLost.cfg is deleted. The verify-reconnect skill is updated.
  • Backoff doc: reworded to match what the Redirects cycle 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.
  • Deletes 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:

  1. A dial in flight to the leaving node is abandoned, and the redirect target is dialed at once. Rejected: letting the handshake finish and applying the redirect only to the next walk. A failed walk restores the configured targets, so that path would still land back on the leaving node, and a handshake that succeeds lands on a node that is going away.
  2. follow does no redirect-loop scoring. Rejected: running it through Redirects::depart and backing off on a loop, as end does. 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 by end when it ends. This keeps Rust and JS simple and in step.
  3. A redirect that arrives during a backoff keeps the wait. Rejected: cutting the backoff short. The wait paces a failure that already happened.
  4. Only an accepted redirect acts. A refused redirect from the draining predecessor is ignored, not terminal as in end, because the policy is already leaving that session. Rejected: ending the policy as end does.

Validation

  • verify-reconnect: Handover.cfg (4 sessions, 1,388,332 distinct states) and RestartWait.cfg (303,814 states) pass every rule, including RedirectFollowed. Mutation check: when ProbeGetsThrough stops moving the watcher, RedirectFollowed is violated again.
  • Rust: residual_a_redirect_after_a_probe_got_through_is_lost is flipped to a_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.
  • JS: new 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 #follow disabled 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.
  • JS: 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 #applyPending removed from the close path.
  • just check passes.

Caused by

  • moq-dev/moq#8330 added the fallback probe, which moved the session into the predecessor without its GOAWAY watcher.

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 review after each round.

  • Head: dfeaaa8

  • Base: origin/main at bd617cc

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

  • Session: 01a1da7e-01c9-77e1-afbe-758276fd46dd, round 2

  • Score: 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:

    • Fixed: medium, 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.
    • Fixed: low, .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.

kixelated and others added 2 commits November 14, 2026 05:15
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>
@kixelated kixelated changed the title A GOAWAY to a draining fallback session is followed fix(moq-net): follow a GOAWAY redirect from a draining fallback session Nov 14, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 14, 2026 14:45
@coderabbitai

coderabbitai Bot commented Nov 14, 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 27 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: 16d0b315-20cd-4edb-a080-bd853b15d7e5
Commits

Reviewing files that changed from the base of the PR and between 77c9ca5 and dfeaaa8.

Files selected for processing: 10

  • .claude/skills/verify-reconnect/SKILL.md
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/reload.ts
  • quest/m1/reconnect-probe-redirect.md
  • rs/moq-net/src/reconnect.rs
  • rs/moq-net/src/reconnect/tests.rs
  • rs/moq-net/verify/reconnect/Handover.cfg
  • rs/moq-net/verify/reconnect/Handover.tla
  • rs/moq-net/verify/reconnect/RedirectLost.cfg
  • rs/moq-net/verify/reconnect/RestartWait.cfg
  • 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

@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 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: probed now moves the fallback's goaway::Consumer into Predecessor::redirect. While no replacement is live, poll acts 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 through goaway::Producer, which writes to separate state from the peer's Consumer, so it never trips this path. moq-tokio handles the new Action::Abandon by dropping the probe-turned-dial held in dial. The dial_failed flag that Abandon sets has no effect, because the next dial resets it or sets it again before Retrying reads it.
  • JS #follow and #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, #tick reruns the effect, and establish closes the reused reached.transport when that run aborts. During a backoff #dialing is false, so the wait is kept, as in Rust.
  • Model and docs: FollowPred and the per-session watched flag model the change, and RedirectFollowed moves into Handover.cfg and RestartWait.cfg. RedirectLost.cfg and the quest file are removed. The Backoff and Action::Abandon docs now describe what the code does.

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

@kixelated
kixelated added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 5725f0c Nov 14, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m1/reconnect-probe-redirect branch November 14, 2026 17:54
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