Skip to content

feat(net)!: resolve the reload backoff the way moq-tokio does - #6756

Merged
kixelated merged 6 commits into
mainfrom
quest/m1/js-reload-config
Oct 25, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m1/js-reload-config

Conversation

@kixelated

@kixelated kixelated commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

@moq/net's reload backoff (ReloadDelay, exposed as Connection.Backoff) disagreed with moq-tokio's connect.backoff field by field:

  • Rust split the healthy bar out of backoff.initial into backoff.healthy (moq-dev/moq#6529), but JS counted any session that outlived delay.initial as healthy.
  • A zero max capped every retry after the first at zero (Rust: uncapped), and a max below initial did not cap the first delay.
  • A multiplier below 1 collapsed or shrank the delay (Rust: treated as 1); an uncapped delay could overflow and stall the loop with an error inside the effect (Rust saturates).
  • Negative or NaN timeout silently meant "unlimited", and other invalid fields failed only later, inside the effect.
  • The retry window counted from the first failure rather than the first dial, dialed once more at the deadline instead of giving up, and a peer restarting through immediate empty GOAWAYs spent the window and could make the loop give up.
  • ReloadProps accepted extensions, which the loop never forwarded to connect().

Approach

  • One pacing() resolves ReloadDelay against its defaults with Rust's Pacing meaning (initial capped by max, zero max uncapped, multiplier at least 1, saturating growth, settled falling back to healthy when uncapped). It validates when the loop is built and is re-resolved per use, so an edit to delay still applies to the next retry.
  • delay.healthy (default 1000ms) gates the backoff/window reset and immediate-GOAWAY damping. Zero is refused, as in Rust.
  • The retry window starts at the first dial of a run. A wait that reaches the deadline gives up there. An immediate GOAWAY naming no URI escalates the backoff but restarts the window after the wait, as Rust's restarting path does.
  • Omit extensions from ReloadProps. The moq-net exports quest makes ConnectProps.extensions @internal, so it is not forwarded.
  • Mocked-time tests: a backoff table (default and healthy: 50 bars, max: 0, max below initial, multiplier 0 and 0.5), saturation, the retry window table (finishes an attempt under way, gives up at the deadline), restart GOAWAYs, and a refusal test for every field's invalid values. Each one except healthy: 50 fails on main. Migrate tests that relied on initial doubling as the bar now set healthy.
  • Deletes the finished quest quest/m1/js-reload-config.md.

Kept different from Rust on purpose: timeout defaults to 0 (unlimited) rather than 10s, since a page usually has nothing that would redial a given-up loop (documented on DEFAULT_DELAY).

Impact

  • @moq/net (breaking): Connection.Backoff gains healthy. A session now counts as healthy after healthy (default 1000ms) rather than after initial. To keep the old behavior, set delay.healthy to your delay.initial.
  • @moq/net (breaking): delay.max: 0 now means no cap instead of a zero delay; pass a small positive max for near-immediate retries. A max below initial now caps the first delay as well.
  • @moq/net (breaking): a multiplier below 1 now keeps the delay constant; use initial: 0 for immediate retries.
  • @moq/net (breaking): a negative, NaN, or infinite initial, max, healthy, or timeout, a zero healthy, or a non-finite multiplier throws RangeError from new Connection / new Reload. Use 0 for an uncapped max or unlimited timeout.
  • @moq/net (behavior): delay.timeout counts from the first dial, so a hanging first dial spends it; a run gives up at the deadline instead of making one last attempt there; a peer draining sessions with an empty GOAWAY no longer spends it. Lengthen timeout by one connect attempt to keep the old reach.
  • @moq/net (internal): ReloadProps no longer accepts extensions, which was ignored. Drop the option.
  • Wire: none.

Alternatives

  • Forward extensions instead of omitting it: rejected while the API audit makes it @internal.
  • Clamp invalid fields instead of refusing them: refused instead, since Rust's types cannot hold them and the repo fails loud on malformed input.

Follow-ups

None.

Checks: just check (js check, js tests, markdown, quest) passed; bun test src/connection/ 214 pass.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 25, 2026 00:14
The reload loop counted a session as healthy once it outlived
`delay.initial`, so the first retry delay also decided which sessions
reset the backoff. Add `delay.healthy` (default 1000ms, must be
positive), matching moq-tokio's `connect.backoff.healthy`.

Also drop `extensions` from `ReloadProps`: the loop never forwarded it
to connect(), so it was accepted and ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title JS reload takes the Rust backoff config feat(net)!: take Rust's healthy backoff bar in the reload loop Oct 25, 2026
@kixelated
kixelated marked this pull request as ready for review October 25, 2026 09:02
@coderabbitai

coderabbitai Bot commented Oct 25, 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 29 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: f4422c46-bed2-4ccf-89da-4053cfa21bf2
Commits

Reviewing files that changed from the base of the PR and between 1308011 and b63d49f.

Files selected for processing: 6
  • js/net/api/index.api.md
  • js/net/src/connection/migrate.test.ts
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/reload.ts
  • quest/m1/README.md
  • quest/m1/js-reload-config.md
  • 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 commented Oct 25, 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-25T17:10:10.221684Z b63d49f New commits
ℹ️ 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.

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed commit: 34e52d6

No actionable bugs found. Separating the healthy-session threshold from the initial retry delay is the right direction: both ordinary disconnects and immediate GOAWAY damping use the new threshold, while redirect-loop detection remains independent. The mocked-time cases distinguish escalating short-lived sessions from healthy resets. Validating the threshold at construction and removing the ignored extensions option keep the configuration clearer.

Verification: static review of the patch, surrounding reconnect/redirect logic, clock validation and public Connection option forwarding. I did not run tests, builds or network scenarios.

A zero `delay.max` capped every retry after the first at zero, so the
loop redialed as fast as it could. moq-tokio treats a zero max as
uncapped and caps the first delay too when max is below initial; do the
same, and fall back to the healthy bar for redirect-loop runs when
uncapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(net)!: take Rust's healthy backoff bar in the reload loop feat(net)!: match Rust's healthy and max backoff in the reload loop Oct 25, 2026

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed commit: f1502af

No new actionable bugs found since the previous review (34e52d6). Treating max: 0 as uncapped and applying a positive cap to the first retry removes the documented configuration inconsistencies. The shared cap helper keeps retry growth and redirect settling coherent, and the expanded mocked-time cases cover both changes. The clock backend already splits oversized host timers, so uncapped growth does not immediately wrap into rapid retries.

Verification: static review of the incremental changes, surrounding retry/redirect logic and clock timer backend. I did not run tests or network scenarios.

Resolve ReloadDelay through one pacing() mirroring Rust's Pacing:
- a multiplier below 1 keeps the delay constant, instead of collapsing
  it to zero or shrinking it;
- an uncapped delay saturates instead of overflowing and stalling the
  loop;
- a negative, NaN, or infinite field (or a non-finite multiplier) is
  refused when the loop is built, instead of silently meaning
  "unlimited" or failing later inside the effect.

Match the retry window too: it counts from the first dial of a run, a
wait that reaches the deadline gives up there instead of dialing, and a
peer restarting through immediate empty GOAWAYs escalates the backoff
but restarts the window rather than spending it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(net)!: match Rust's healthy and max backoff in the reload loop feat(net)!: resolve the reload backoff the way moq-tokio does Oct 25, 2026

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed commit: 58b5379

No new actionable bugs found since reviewed f1502af. Centralizing pacing validation and growth is clearer than maintaining separate interpretations across retry and migration paths. The update adds multiplier clamping and overflow-safe growth, starts the retry budget with the first dial, and distinguishes empty-URI restarts from failed redirects while retaining escalating backoff. The new tests cover deadline boundaries, invalid configuration and repeated restarts.

Verification: static review of the incremental changes, surrounding retry/migration logic and corresponding Rust pacing/restart behavior. I did not run tests or exercise browser/network timing.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merged main and refreshed the @moq/net API report that main added (moq-dev/moq#6477) for the new ReloadDelay.healthy. No other changes.

just check (js, markdown, gh, quest) and quest check --base origin/main pass. Landing.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 1f5a58c Oct 25, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/js-reload-config branch October 25, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant