From 8298fb2794da6e8ae7f95f5e35f7ed63645f5f79 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 11:47:15 +0000 Subject: [PATCH] fix(proof): never take the socket from a busy read server `proof serve` probes its socket before it starts, and takes the socket over when nothing answers. The probe gave up after two seconds. But a server building a large graph, such as the benchmark's 10x graph, blocks for six to nine seconds, so a second server took its socket and left the first one running and unreachable. A dead socket refuses the connection at once, while a busy server accepts it and replies late. The probe now counts a timeout as a live server. A new test holds a socket with a server that never replies, and checks that `serveProofReads` refuses it. The test fails without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DVVnhYL1Bz7mCfEKDoyLMW --- packages/flatbread/src/cli/serve.test.ts | 23 +++++++++++++++++++++++ packages/flatbread/src/cli/serve.ts | 9 +++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/flatbread/src/cli/serve.test.ts b/packages/flatbread/src/cli/serve.test.ts index 768b8f61..0c773d51 100644 --- a/packages/flatbread/src/cli/serve.test.ts +++ b/packages/flatbread/src/cli/serve.test.ts @@ -11,6 +11,7 @@ import { unlink, writeFile, } from 'node:fs/promises'; +import { createServer } from 'node:http'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { handleEffortWrite } from './proof.js'; @@ -300,3 +301,25 @@ test.serial( t.regex(JSON.parse(result.stdout).summary, /^0 records/); } ); + +test.serial( + 'a second server does not take the socket from a busy one', + async (t) => { + const dir = await mkdtemp(join(tmpdir(), 'flatbread-busy-')); + t.teardown(() => rm(dir, { recursive: true, force: true })); + const socket = join(dir, 'serve.sock'); + // A server that accepts connections but never replies, like one + // blocked while it builds a large graph. + const busy = createServer(() => undefined); + const held = new Set(); + busy.on('connection', (connection) => held.add(connection)); + await new Promise((done) => busy.listen(socket, done)); + t.teardown(() => { + for (const connection of held) connection.destroy(); + busy.close(); + }); + await t.throwsAsync(serveProofReads(socket), { + message: /already answers on/, + }); + } +); diff --git a/packages/flatbread/src/cli/serve.ts b/packages/flatbread/src/cli/serve.ts index 3f22924a..e0b5d69a 100644 --- a/packages/flatbread/src/cli/serve.ts +++ b/packages/flatbread/src/cli/serve.ts @@ -47,7 +47,12 @@ export interface ServedRead { readonly code: number; } -/** True when something answers HTTP on `socketPath`. */ +/** + * True when a server holds `socketPath`. A dead socket refuses the connection + * at once. A live server that is busy, such as one building a large graph, + * accepts it but may not reply within the probe's two seconds, so a timeout + * also counts as live. + */ function answers(socketPath: string): Promise { return new Promise((resolveAnswer) => { const probe = request( @@ -60,7 +65,7 @@ function answers(socketPath: string): Promise { probe.on('error', () => resolveAnswer(false)); probe.setTimeout(2000, () => { probe.destroy(); - resolveAnswer(false); + resolveAnswer(true); }); probe.end(); });