perf: reuse SSH connections per server, with a retry rule that never repeats work
Every ssh.exec opened its own connection: a TCP handshake, a key exchange and an
authentication round trip per command. A key rotation paid for that eight times,
a deployment six, and refreshing M profile states M times.
Connections are now kept per server. The three risks that made this worth doing
carefully are handled explicitly:
- Staleness. A pooled connection can be dead exactly when it matters. Liveness is
tracked through error, close and end, and a lease that finds a dead entry opens
a new one. The remaining race, where the connection dies between the check and
the command, is caught by the retry rule below.
- Retrying. Only a failure that proves the command never reached the server is
retried, and only once, and only on a connection that was already established
before this call. execClient marks exactly that case, when the channel fails to
open. A command that opened a stream is never repeated, because the server may
already be acting on it - repeating a deployment is not this layer's decision.
Two tests hold that line: widening the rule to any failure fails both.
- Lifetime. Idle connections close after a minute, the pool is reference counted
so a shared connection survives until its last user is done, closeAll runs
during quit, and every pooled client keeps a standing error listener so an
error while idle cannot reach the uncaughtException handler.
A trust-on-first-use connection is never pooled: it was established without
verifying the fingerprint, so it must not serve a later verified call. A change
to host, port, user, auth type, key path or trusted fingerprint invalidates the
pooled connection.
ssh-service coverage rises from 61% to 90% of lines and 97% of functions.
Also in this commit, the smaller items from the same review:
- Diagnostics batched records that queue up while a write is in flight into one
append, and chmod runs once per file instead of once per record. At the debug
level every IPC call writes a line, which is exactly when troubleshooting.
- The set that suppresses duplicate deployment notifications is trimmed instead
of growing for the lifetime of the process.
- The updater kept the same once('error') pattern on its spawned helper that
took the app down through the SSH client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
beeafdcba7
commit
cb9bdcd713
@@ -46,6 +46,9 @@ class DiagnosticsService {
|
||||
this.preferencesProvider = preferencesProvider;
|
||||
this.sessionId = crypto.randomUUID();
|
||||
this.writeChain = Promise.resolve();
|
||||
this.pendingLines = [];
|
||||
this.pendingFlush = null;
|
||||
this.securedFiles = new Set();
|
||||
this.initialized = false;
|
||||
this.lastWriteError = null;
|
||||
this.lastBundlePath = null;
|
||||
@@ -115,13 +118,25 @@ class DiagnosticsService {
|
||||
sessionId: this.sessionId,
|
||||
details
|
||||
});
|
||||
const line = `${JSON.stringify(record)}\n`;
|
||||
this.writeChain = this.writeChain.then(async () => {
|
||||
this.pendingLines.push(`${JSON.stringify(record)}\n`);
|
||||
// At the debug level every IPC call and every Gitea request writes a line.
|
||||
// Records that queue up while a write is in flight are appended together, so
|
||||
// a burst costs one open/write/close instead of one per record.
|
||||
if (this.pendingFlush) return this.pendingFlush;
|
||||
this.pendingFlush = this.writeChain.then(async () => {
|
||||
this.pendingFlush = null;
|
||||
const lines = this.pendingLines.splice(0).join('');
|
||||
if (!lines) return true;
|
||||
try {
|
||||
if (!this.initialized) await fs.mkdir(this.logDirectory, { recursive: true, mode: 0o700 });
|
||||
const target = await this.rotateIfNeeded(this.filePathForToday());
|
||||
await fs.appendFile(target, line, { encoding: 'utf8', mode: 0o600 });
|
||||
try { await fs.chmod(target, 0o600); } catch {}
|
||||
await fs.appendFile(target, lines, { encoding: 'utf8', mode: 0o600 });
|
||||
// The mode above only applies when appendFile creates the file, so the
|
||||
// explicit chmod is needed once per file rather than once per record.
|
||||
if (!this.securedFiles.has(target)) {
|
||||
try { await fs.chmod(target, 0o600); } catch { /* best effort */ }
|
||||
this.securedFiles.add(target);
|
||||
}
|
||||
this.lastWriteError = null;
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -129,7 +144,8 @@ class DiagnosticsService {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return this.writeChain;
|
||||
this.writeChain = this.pendingFlush.catch(() => {});
|
||||
return this.pendingFlush;
|
||||
}
|
||||
|
||||
debug(event, details) { return this.log('debug', event, details); }
|
||||
|
||||
Reference in New Issue
Block a user