Skip to content

Commit 9cdda18

Browse files
committed
🤖 fix: expire idle masters started by ensureReadyMaster
1 parent 09071ca commit 9cdda18

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/node/runtime/openSshMasterPool.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,55 @@ describe("OpenSSHMasterPool", () => {
555555
pool.clearAll();
556556
});
557557

558+
test("ensureReadyMaster schedules idle cleanup after bootstrapping a shard", async () => {
559+
const pool = new OpenSSHMasterPool({
560+
maxSessionsPerShard: 1,
561+
maxShardsPerHost: 1,
562+
sleep: () => Promise.resolve(),
563+
spawnProcess: ((_command: string, args?: readonly string[]) => {
564+
const proc = new FakeChildProcess();
565+
const normalizedArgs = [...(args ?? [])];
566+
567+
if (normalizedArgs.includes("-M")) {
568+
const controlPathArg = normalizedArgs.find((arg) => arg.startsWith("ControlPath="));
569+
if (controlPathArg) {
570+
masterProcesses.set(controlPathArg.slice("ControlPath=".length), proc);
571+
}
572+
return proc as never;
573+
}
574+
575+
const controlPathIndex = normalizedArgs.indexOf("-S");
576+
const controlPath =
577+
controlPathIndex >= 0 ? normalizedArgs[controlPathIndex + 1] : undefined;
578+
queueMicrotask(() => {
579+
if (normalizedArgs.includes("check") && controlPath && masterProcesses.has(controlPath)) {
580+
proc.exitCode = 0;
581+
}
582+
proc.emit("close", proc.exitCode ?? 1, null);
583+
});
584+
return proc as never;
585+
}) as unknown as typeof spawnProcess,
586+
});
587+
588+
const config: SSHConnectionConfig = { host: "remote.example.com", port: 22 };
589+
await pool.ensureReadyMaster(config, { maxWaitMs: 0, timeoutMs: 1000 });
590+
591+
const internals = pool as unknown as {
592+
hostGroups: Map<
593+
string,
594+
{ shards: Array<{ inflight: number; idleTimer?: ReturnType<typeof setTimeout> }> }
595+
>;
596+
};
597+
const shard = [...internals.hostGroups.values()][0]?.shards[0];
598+
if (!shard) {
599+
throw new Error("Expected a tracked shard");
600+
}
601+
expect(shard.inflight).toBe(0);
602+
expect(shard.idleTimer).toBeDefined();
603+
604+
pool.clearAll();
605+
});
606+
558607
test("retries transient shard startup failures within the maxWait budget", async () => {
559608
let startupAttempts = 0;
560609
const pool = new OpenSSHMasterPool({

src/node/runtime/openSshMasterPool.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ export class OpenSSHMasterPool {
184184
}
185185

186186
this.trimExitedShards(hostGroup);
187-
if (this.pickReadyShard(hostGroup)) {
187+
const readyShard = this.pickReadyShard(hostGroup);
188+
if (readyShard) {
189+
this.scheduleIdleDisposalIfUnused(readyShard);
188190
return;
189191
}
190192

@@ -202,6 +204,7 @@ export class OpenSSHMasterPool {
202204
? defaultStartTimeoutMs
203205
: Math.min(defaultStartTimeoutMs, Math.max(1, deadlineMs - Date.now()));
204206
await this.startShard(hostGroup, restartable, startupTimeoutMs, options?.abortSignal);
207+
this.scheduleIdleDisposalIfUnused(restartable);
205208
return;
206209
} catch (error) {
207210
if (options?.abortSignal?.aborted) {
@@ -219,6 +222,7 @@ export class OpenSSHMasterPool {
219222
? defaultStartTimeoutMs
220223
: Math.min(defaultStartTimeoutMs, Math.max(1, deadlineMs - Date.now()));
221224
await this.startShard(hostGroup, shard, startupTimeoutMs, options?.abortSignal);
225+
this.scheduleIdleDisposalIfUnused(shard);
222226
return;
223227
} catch (error) {
224228
if (options?.abortSignal?.aborted) {
@@ -437,6 +441,12 @@ export class OpenSSHMasterPool {
437441
};
438442
}
439443

444+
private scheduleIdleDisposalIfUnused(shard: MasterShard): void {
445+
if (shard.inflight === 0) {
446+
this.scheduleIdleDisposal(shard);
447+
}
448+
}
449+
440450
private scheduleIdleDisposal(shard: MasterShard): void {
441451
clearTimeout(shard.idleTimer);
442452
shard.idleTimer = setTimeout(() => {

0 commit comments

Comments
 (0)