Skip to content

Commit c69716c

Browse files
committed
refactor: default SSH acquireConnection to wait
1 parent f9dfe6b commit c69716c

File tree

5 files changed

+25
-60
lines changed

5 files changed

+25
-60
lines changed

src/node/runtime/Runtime.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,6 @@ export interface ExecOptions {
6565
abortSignal?: AbortSignal;
6666
/** Force PTY allocation (SSH only - adds -t flag) */
6767
forcePTY?: boolean;
68-
69-
/**
70-
* SSH only: if set (>0), wait through SSHConnectionPool backoff (bounded).
71-
*
72-
* If not set, SSH operations fail fast when the host is in backoff.
73-
*/
74-
connectionMaxWaitMs?: number;
75-
76-
/** SSH only: invoked when waiting due to SSH backoff. */
77-
onConnectionWait?: (waitMs: number) => void;
7868
}
7969

8070
/**

src/node/runtime/SSHRuntime.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,6 @@ function logSSHBackoffWait(initLogger: InitLogger, waitMs: number): void {
4747
}
4848

4949
// Re-export SSHRuntimeConfig from connection pool (defined there to avoid circular deps)
50-
const USER_INITIATED_SSH_MAX_WAIT_MS = 2 * 60 * 1000;
51-
52-
function userInitiatedSSHWaitExecOptions(
53-
initLogger: InitLogger
54-
): Pick<ExecOptions, "connectionMaxWaitMs" | "onConnectionWait"> {
55-
return {
56-
connectionMaxWaitMs: USER_INITIATED_SSH_MAX_WAIT_MS,
57-
onConnectionWait: (waitMs) => logSSHBackoffWait(initLogger, waitMs),
58-
};
59-
}
60-
6150
export type { SSHRuntimeConfig } from "./sshConnectionPool";
6251

6352
/**
@@ -140,15 +129,9 @@ export class SSHRuntime implements Runtime {
140129

141130
// Ensure connection is healthy before executing.
142131
// This provides backoff protection and singleflighting for concurrent requests.
143-
if (options.connectionMaxWaitMs !== undefined && options.connectionMaxWaitMs > 0) {
144-
await sshConnectionPool.acquireConnection(this.config, {
145-
maxWaitMs: options.connectionMaxWaitMs,
146-
abortSignal: options.abortSignal,
147-
onWait: options.onConnectionWait,
148-
});
149-
} else {
150-
await sshConnectionPool.acquireConnection(this.config);
151-
}
132+
await sshConnectionPool.acquireConnection(this.config, {
133+
abortSignal: options.abortSignal,
134+
});
152135

153136
// Build command parts
154137
const parts: string[] = [];
@@ -649,7 +632,6 @@ export class SSHRuntime implements Runtime {
649632

650633
// Step 2: Ensure the SSH host is reachable before doing expensive local work
651634
await sshConnectionPool.acquireConnection(this.config, {
652-
maxWaitMs: USER_INITIATED_SSH_MAX_WAIT_MS,
653635
abortSignal,
654636
onWait: (waitMs) => logSSHBackoffWait(initLogger, waitMs),
655637
});
@@ -718,7 +700,6 @@ export class SSHRuntime implements Runtime {
718700
cwd: "~",
719701
timeout: 300, // 5 minutes for clone
720702
abortSignal,
721-
...userInitiatedSSHWaitExecOptions(initLogger),
722703
});
723704

724705
const [cloneStdout, cloneStderr, cloneExitCode] = await Promise.all([
@@ -741,7 +722,6 @@ export class SSHRuntime implements Runtime {
741722
cwd: "~",
742723
timeout: 30,
743724
abortSignal,
744-
...userInitiatedSSHWaitExecOptions(initLogger),
745725
}
746726
);
747727
await createTrackingBranchesStream.exitCode;
@@ -756,7 +736,6 @@ export class SSHRuntime implements Runtime {
756736
cwd: "~",
757737
timeout: 10,
758738
abortSignal,
759-
...userInitiatedSSHWaitExecOptions(initLogger),
760739
}
761740
);
762741

@@ -775,7 +754,6 @@ export class SSHRuntime implements Runtime {
775754
cwd: "~",
776755
timeout: 10,
777756
abortSignal,
778-
...userInitiatedSSHWaitExecOptions(initLogger),
779757
}
780758
);
781759
await removeOriginStream.exitCode;
@@ -787,7 +765,6 @@ export class SSHRuntime implements Runtime {
787765
cwd: "~",
788766
timeout: 10,
789767
abortSignal,
790-
...userInitiatedSSHWaitExecOptions(initLogger),
791768
});
792769

793770
const rmExitCode = await rmStream.exitCode;
@@ -803,7 +780,6 @@ export class SSHRuntime implements Runtime {
803780
cwd: "~",
804781
timeout: 10,
805782
abortSignal,
806-
...userInitiatedSSHWaitExecOptions(initLogger),
807783
});
808784
await rmStream.exitCode;
809785
} catch {
@@ -842,7 +818,6 @@ export class SSHRuntime implements Runtime {
842818
timeout: 3600, // 1 hour - generous timeout for init hooks
843819
abortSignal,
844820
env: muxEnv,
845-
...userInitiatedSSHWaitExecOptions(initLogger),
846821
});
847822

848823
// Create line-buffered loggers
@@ -916,7 +891,6 @@ export class SSHRuntime implements Runtime {
916891
cwd: "/tmp",
917892
timeout: 10,
918893
abortSignal,
919-
...userInitiatedSSHWaitExecOptions(initLogger),
920894
});
921895
const mkdirExitCode = await mkdirStream.exitCode;
922896
if (mkdirExitCode !== 0) {
@@ -979,7 +953,6 @@ export class SSHRuntime implements Runtime {
979953
cwd: workspacePath, // Use the full workspace path for git operations
980954
timeout: 300, // 5 minutes for git checkout (can be slow on large repos)
981955
abortSignal,
982-
...userInitiatedSSHWaitExecOptions(initLogger),
983956
});
984957

985958
const [stdout, stderr, exitCode] = await Promise.all([
@@ -1044,7 +1017,6 @@ export class SSHRuntime implements Runtime {
10441017
cwd: workspacePath,
10451018
timeout: 120, // 2 minutes for network operation
10461019
abortSignal,
1047-
...userInitiatedSSHWaitExecOptions(initLogger),
10481020
});
10491021

10501022
const fetchExitCode = await fetchStream.exitCode;
@@ -1064,7 +1036,6 @@ export class SSHRuntime implements Runtime {
10641036
cwd: workspacePath,
10651037
timeout: 60, // 1 minute for fast-forward merge
10661038
abortSignal,
1067-
...userInitiatedSSHWaitExecOptions(initLogger),
10681039
});
10691040

10701041
const [mergeStderr, mergeExitCode] = await Promise.all([

src/node/runtime/sshConnectionPool.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ describe("SSHConnectionPool", () => {
193193
};
194194

195195
// Trigger a failure via acquireConnection (will fail to connect)
196-
await expect(pool.acquireConnection(config, 1000)).rejects.toThrow();
196+
await expect(
197+
pool.acquireConnection(config, { timeoutMs: 1000, maxWaitMs: 0 })
198+
).rejects.toThrow();
197199

198200
// Verify we're now in backoff
199201
const healthBefore = pool.getConnectionHealth(config);
@@ -231,7 +233,7 @@ describe("SSHConnectionPool", () => {
231233
expect(elapsed).toBeLessThan(50);
232234
});
233235

234-
test("wait mode waits through backoff (bounded) instead of throwing", async () => {
236+
test("waits through backoff (bounded) instead of throwing", async () => {
235237
const pool = new SSHConnectionPool();
236238
const config: SSHRuntimeConfig = {
237239
host: "test.example.com",
@@ -246,7 +248,6 @@ describe("SSHConnectionPool", () => {
246248
const onWaitCalls: number[] = [];
247249

248250
await pool.acquireConnection(config, {
249-
maxWaitMs: 60_000,
250251
onWait: (ms) => {
251252
onWaitCalls.push(ms);
252253
},
@@ -272,10 +273,12 @@ describe("SSHConnectionPool", () => {
272273
};
273274

274275
// Trigger a failure to put connection in backoff
275-
await expect(pool.acquireConnection(config, 1000)).rejects.toThrow();
276+
await expect(
277+
pool.acquireConnection(config, { timeoutMs: 1000, maxWaitMs: 0 })
278+
).rejects.toThrow();
276279

277280
// Second call should throw immediately with backoff message
278-
await expect(pool.acquireConnection(config)).rejects.toThrow(/in backoff/);
281+
await expect(pool.acquireConnection(config, { maxWaitMs: 0 })).rejects.toThrow(/in backoff/);
279282
});
280283

281284
test("getControlPath returns deterministic path", () => {
@@ -303,9 +306,9 @@ describe("SSHConnectionPool", () => {
303306

304307
// All concurrent calls should share the same probe and get same result
305308
const results = await Promise.allSettled([
306-
pool.acquireConnection(config, 1000),
307-
pool.acquireConnection(config, 1000),
308-
pool.acquireConnection(config, 1000),
309+
pool.acquireConnection(config, { timeoutMs: 1000, maxWaitMs: 0 }),
310+
pool.acquireConnection(config, { timeoutMs: 1000, maxWaitMs: 0 }),
311+
pool.acquireConnection(config, { timeoutMs: 1000, maxWaitMs: 0 }),
309312
]);
310313

311314
// All should be rejected (connection fails)

src/node/runtime/sshConnectionPool.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,17 @@ const BACKOFF_SCHEDULE = [1, 5, 10, 20, 40, 60];
6565
const HEALTHY_TTL_MS = 15 * 1000; // 15 seconds
6666

6767
const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
68+
const DEFAULT_MAX_WAIT_MS = 2 * 60 * 1000; // 2 minutes
6869

6970
export interface AcquireConnectionOptions {
7071
/** Timeout for the health check probe. */
7172
timeoutMs?: number;
7273

7374
/**
74-
* If set (>0), acquireConnection will wait through backoff (bounded by maxWaitMs)
75-
* instead of throwing immediately.
75+
* Max time to wait (ms) for a host to become healthy (waits + probes).
76+
*
77+
* - Omit to use the default (waits through backoff).
78+
* - Set to 0 to fail fast.
7679
*/
7780
maxWaitMs?: number;
7881

@@ -136,12 +139,10 @@ export class SSHConnectionPool {
136139
/**
137140
* Ensure connection is healthy before proceeding.
138141
*
139-
* The pool is intentionally conservative by default (fail-fast) to avoid turning
140-
* an SSH outage into app-wide latency regressions.
142+
* By default, acquireConnection waits through backoff (bounded) so user-facing
143+
* actions don’t immediately fail during transient SSH outages.
141144
*
142-
* For user-initiated flows where waiting is preferable to an immediate error
143-
* (workspace init, terminal spawn, explicit user commands), callers can opt in
144-
* to waiting through backoff by providing `maxWaitMs`.
145+
* Callers can opt into fail-fast behavior by passing `{ maxWaitMs: 0 }`.
145146
*/
146147
async acquireConnection(config: SSHRuntimeConfig, timeoutMs?: number): Promise<void>;
147148
async acquireConnection(
@@ -160,8 +161,8 @@ export class SSHConnectionPool {
160161
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
161162
const sleep = options.sleep ?? sleepWithAbort;
162163

163-
const shouldWait = options.maxWaitMs !== undefined && options.maxWaitMs > 0;
164-
const maxWaitMs = options.maxWaitMs ?? 0;
164+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
165+
const shouldWait = maxWaitMs > 0;
165166

166167
const key = makeConnectionKey(config);
167168
const startTime = Date.now();

src/node/services/ptyService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export class PTYService {
216216

217217
// Ensure connection is healthy before spawning terminal
218218
// This provides backoff protection and singleflighting for concurrent requests
219-
await sshConnectionPool.acquireConnection(sshConfig, { maxWaitMs: 30_000 });
219+
await sshConnectionPool.acquireConnection(sshConfig);
220220

221221
const sshArgs = buildSSHArgs(sshConfig, workspacePath);
222222

0 commit comments

Comments
 (0)