diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 461509ea0ad2..a585ec3fbdd2 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -23,6 +23,7 @@ import { launchOrReuseRemoteServer, REMOTE_PICK_PORT_SCRIPT, SshEnvironmentManager, + SSH_TUNNEL_READY_PROBE_TIMEOUT_MS, waitForHttpReady, } from "./tunnel.ts"; @@ -310,6 +311,37 @@ describe("ssh tunnel scripts", () => { ), ); + const slowHttpClient = HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, new Response("", { status: 200 }))).pipe( + Effect.delay(Duration.millis(1_500)), + ), + ); + + it.effect( + "succeeds on a single response slower than 1s using the tunnel probe bound, so a high-RTT link only makes readiness slower, not impossible", + () => + Effect.gen(function* () { + const fiber = yield* Effect.forkChild( + Effect.result( + waitForHttpReady({ + baseUrl: "http://127.0.0.1:41773/", + timeoutMs: 5_000, + probeTimeoutMs: SSH_TUNNEL_READY_PROBE_TIMEOUT_MS, + }), + ), + ); + yield* Effect.yieldNow; + yield* TestClock.adjust(Duration.millis(5_000)); + + const result = yield* Fiber.join(fiber); + assert.isTrue(Result.isSuccess(result)); + }).pipe( + Effect.provide( + Layer.merge(TestClock.layer(), Layer.succeed(HttpClient.HttpClient, slowHttpClient)), + ), + ), + ); + it("preserves primitive readiness reason values in diagnostic output", () => { assert.deepEqual( describeReadinessCause({ diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 12ab0027803c..8959edcc2dbc 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -52,7 +52,24 @@ import { export const DEFAULT_REMOTE_PORT = 3773; const REMOTE_PORT_SCAN_WINDOW = 200; const SSH_READY_TIMEOUT_MS = 20_000; +// Default probe bound for waitForHttpReady, and the value fed to the +// remote-side scripts' own loopback probe (T3_READY_PROBE_TIMEOUT_MS). That +// remote probe's deadline loop (REMOTE_WAIT_READY_SCRIPT) only checks its +// overall deadline *between* attempts — a single in-flight probe isn't +// interrupted when the deadline passes — so this must stay comfortably under +// REMOTE_REUSE_READY_TIMEOUT_MS (2s) or one slow attempt can blow through the +// whole reuse-check budget. It never needs to be larger: this probe is always +// loopback on the remote host, with no RTT to account for. const SSH_READY_PROBE_TIMEOUT_MS = 1_000; +// Bound for the desktop-side probe that crosses the real SSH link (desktop -> +// tunnel -> remote), used only at the initial-connect readiness check below. +// On a high-RTT connection (satellite/in-flight wifi) a single request can +// legitimately take longer than a typical LAN round trip. Safe to set well +// above any of the timeouts above: unlike the remote script's loop, Effect's +// own timeoutOption (in the shared waitForHttpReady) genuinely interrupts an +// in-flight request once its outer deadline elapses, so this can't make a +// short outer timeoutMs elsewhere overshoot. +export const SSH_TUNNEL_READY_PROBE_TIMEOUT_MS = 8_000; const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000; const REMOTE_READY_TIMEOUT_MS = 60_000; const REMOTE_LAUNCH_TIMEOUT_MS = 90_000; @@ -1100,6 +1117,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: waitForHttpReady({ baseUrl: input.httpBaseUrl, timeoutMs: SSH_READY_TIMEOUT_MS, + probeTimeoutMs: SSH_TUNNEL_READY_PROBE_TIMEOUT_MS, }), exitFailure, ).pipe(