Skip to content

fix: reconnect faster after remote server updates - #5404

Open
t3dotgg wants to merge 1 commit into
mainfrom
t3code/faster-update-reconnect
Open

fix: reconnect faster after remote server updates#5404
t3dotgg wants to merge 1 commit into
mainfrom
t3code/faster-update-reconnect

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 5, 2026

Copy link
Copy Markdown
Member

A remote server update restarts the server for about 15 seconds, but the client shows "Resuming" for up to ~33s. Separately, the relay tunnel becomes reachable ~5s after the new server starts listening.

Measured from boot-service.log across recent updates: old server dies at T+2s, new server listens at T+7-9s, relay tunnel reachable at T+13-15s.

Client: nudge reconnect for the whole restart, not just once

The update flow forked a fiber that waited for the first backoff state and called retryNow once. That nudge lands at ~T+2s while the server is still down for another ~11s, so it is wasted. The supervisor then climbs its normal 1/2/4/8/16s ladder and attempts land at ~3, 5, 9, 17 and 33s, which is how a ~15s restart becomes a ~33s wait.

Now it nudges on every backoff entry, with a one-second sleep before each one, so attempts stay ~1s apart for the whole restart window. The sleep is the pacer: a connection that fails instantly (ECONNREFUSED, or a 502 through the relay) re-enters backoff immediately and would otherwise spin a tight retry loop.

The fiber stays Effect.forkChild, so it is interrupted as soon as the update command settles. With that in place the 30s cap is unnecessary and now matches SERVER_UPDATE_RESUME_TIMEOUT, covering the realistic restart window instead of cutting off at 30s.

The loop is extracted as nudgeReconnectDuringUpdateRestart so it is testable without standing up an AtomRegistry; two tests cover the repeated-nudge count and the pacing.

Server: what the ~5s actually is

Worth stating plainly, since it changes what is fixable here: the ~5s between Listening on http://127.0.0.1:3773 and Relay client process started is not local serialization behind activation. Activation opens ~50ms after the listen log, and nothing is logged in the remaining window. The time is spent inside the two relay calls in reconcileDesiredCloudLinkWith, dominated by server-side Cloudflare control-plane work on POST /v1/client/environment-links — a serial chain of tunnel create, configure, and DNS mutations. Measured locally: secret store I/O is under 1ms total, the ed25519 keypair is genuinely first-run-only, and network transport for both POSTs is ~0.6s.

That chain runs on every boot because releaseManagedTunnelOnShutdown deletes the tunnel at shutdown (a deliberate Cloudflare per-tunnel billing tradeoff), so the next startup always pays full tunnel-creation latency instead of reusing an existing tunnel.

So moving the reconcile earlier would not have bought the 5 seconds — the work is remote and already starts promptly. Keeping the reconcile gated on activation is also the safe choice: it preserves the finalizer ownership rule at server.ts:564 and keeps a rolling-back trial from fighting the previous version for the tunnel. I left that gate alone and removed the one piece of local dead time on the path, the 250ms sleep in front of the first attempt. Routes are already serving when activation opens the gate, and the retry schedule covers what the sleep hedged against.

The real leverage is relay-side (keep the tunnel alive across restarts, or parallelize the independent legs of provision), which is out of scope here.

Validation

  • packages/client-runtime tests: 524 passed
  • apps/server tests: 1846 passed, 7 skipped
  • typecheck and lint clean

Note: running the server suite on a machine with a live T3 server leaks T3_SERVICE_LAUNCHER_CONTEXT into the test process and fails 125 tests on main as well. Unsetting it makes the suite pass on both main and this branch.


Made by Claude Fable 5 running in Claude Code.


Note

Medium Risk
Touches update/reconnect timing and server cloud-link startup reconcile—user-visible recovery paths but localized changes with new tests on the client nudge logic.

Overview
Shortens "Resuming" after a remote server self-update by keeping client reconnect attempts on a ~1s cadence for the whole restart, and trims a small server startup delay before cloud link reconcile.

Client: Replaces the one-shot "first backoffretryNow" fiber with nudgeReconnectDuringUpdateRestart, which reacts to every supervisor backoff during the update (1s sleep before each nudge so instant failures do not spin). The nudge fiber still forks as a child of the update command and now times out with SERVER_UPDATE_RESUME_TIMEOUT instead of 30s. Unit tests cover repeated nudges and pacing.

Server: Drops the 250ms delay before the first reconcileDesiredCloudLink attempt after activation; exponential retry on reconcile is unchanged.

Reviewed by Cursor Bugbot for commit 4dedb6a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconnect faster during remote server updates by retrying once per second across the restart window

  • Previously, on a server update restart, the client issued a single immediate retry and then fell back to the supervisor's escalating backoff schedule.
  • Adds nudgeReconnectDuringUpdateRestart in server.ts, which watches connection state changes and triggers a retry for every backoff phase at ~1s intervals until SERVER_UPDATE_RESUME_TIMEOUT is reached.
  • Also removes a 250ms settling delay before the first reconcileDesiredCloudLink call in server.ts, so the initial reconciliation starts immediately.
  • Behavioral Change: connections during a server update now retry roughly once per second for the full restart window instead of once immediately then at escalating intervals.
📊 Macroscope summarized 4dedb6a. 2 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

A remote update restarts the server for ~15 seconds, but the client shows
"Resuming" for up to ~33s and the relay tunnel comes up ~5s behind the
HTTP listener.

The client forked a fiber that nudged the connection supervisor once, on
the first backoff entry. That nudge fires at ~T+2s while the server is
still down for another ~11s, so it is wasted, and the supervisor then
climbs its 1/2/4/8/16s ladder with attempts landing at ~3, 5, 9, 17 and
33s. Nudge on every backoff entry instead, paced one second apart, so
attempts stay ~1s apart for the whole restart window. The fiber stays a
child of the update command, so it is interrupted as soon as the update
settles.

On the server, drop the 250ms sleep in front of the startup cloud link
reconcile. Routes are already serving when activation opens that gate,
and the retry schedule covers what the sleep hedged against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d6123d54-311a-4a58-97a8-f6dd5c72e3ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 5, 2026
* Callers fork this as a child of the update command so it is interrupted as
* soon as the update settles, whether it succeeds, fails, or times out.
*/
export function nudgeReconnectDuringUpdateRestart(input: {

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.

🟠 High state/server.ts:166

The sleep-then-retry in nudgeReconnectDuringUpdateRestart can tear down a connection it just succeeded in establishing. After filtering a backoff event, the function sleeps for ~1s and then fires retryNow unconditionally. If the supervisor reconnects on its own during that sleep, the stale nudge still sends a retry signal, which causes monitorConnectedLease to return false and tear down the new lease — discarding the successful connection before the lifecycle ready event arrives. Consider re-checking the supervisor state (or making retryNow a no-op when not in backoff) before issuing the nudge.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/client-runtime/src/state/server.ts around line 166:

The sleep-then-retry in `nudgeReconnectDuringUpdateRestart` can tear down a connection it just succeeded in establishing. After filtering a `backoff` event, the function sleeps for ~1s and then fires `retryNow` unconditionally. If the supervisor reconnects on its own during that sleep, the stale nudge still sends a retry signal, which causes `monitorConnectedLease` to return `false` and tear down the new lease — discarding the successful connection before the lifecycle `ready` event arrives. Consider re-checking the supervisor state (or making `retryNow` a no-op when not in `backoff`) before issuing the nudge.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4dedb6a. Configure here.

),
Effect.timeoutOption(SERVER_UPDATE_RESUME_TIMEOUT),
Effect.ignore,
);

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.

Stale reconnect nudge race

Medium Severity

nudgeReconnectDuringUpdateRestart sleeps on each backoff entry and then always calls retryNow, without checking whether the supervisor is still backing off. retryNow also tears down an active session, so a reconnect that succeeds during that sleep—common when SubscriptionRef.changes joins mid-backoff or the supervisor’s own 1s timer wins the race—gets interrupted and the resume wait stretches again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4dedb6a. Configure here.

@macroscopeapp

macroscopeapp Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. Two unresolved review comments identify a potential race condition where the new reconnect nudge could tear down a successfully established connection if the supervisor reconnects during the 1-second sleep. This bug concern warrants human review before merging.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant