Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/ssh/src/tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
launchOrReuseRemoteServer,
REMOTE_PICK_PORT_SCRIPT,
SshEnvironmentManager,
SSH_TUNNEL_READY_PROBE_TIMEOUT_MS,
waitForHttpReady,
} from "./tunnel.ts";

Expand Down Expand Up @@ -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({
Expand Down
18 changes: 18 additions & 0 deletions packages/ssh/src/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading