diff --git a/src/js/internal/sql/shared.ts b/src/js/internal/sql/shared.ts index 94e82acef85..bfdcae7f0bc 100644 --- a/src/js/internal/sql/shared.ts +++ b/src/js/internal/sql/shared.ts @@ -631,6 +631,19 @@ abstract class BasePooledConnection { + let handshook = false; + socket.setNoDelay(true); + socket.on("data", () => { + if (handshook) return; + handshook = true; + // Full handshake + admin-shutdown error + FIN — the same close pattern + // pg_terminate_backend produces. + socket.write(handshakeResponse); + socket.write(adminShutdown); + socket.end(); + }); + socket.on("error", () => {}); +}); + +await new Promise(r => server.listen(0, "127.0.0.1", r)); +const port = server.address().port; + +const sql = new SQL({ + url: \`postgres://u@127.0.0.1:\${port}/db\`, + max: 10, + connectionTimeout: 2, +}); + +// A broken pool throws "connection must be a PostgresSQLConnection" on every +// subsequent query once a ghost entry has leaked into readyConnections. A +// healthy pool just keeps seeing "Connection closed" (or the admin-shutdown +// error) because our fake server kicks every connection. A handful of +// iterations is enough to trigger the race reliably; bail out on the first +// occurrence so we don't hang against a corrupted pool. +let corrupted = false; +let iterations = 0; +for (let i = 0; i < 20; i++) { + iterations = i + 1; + try { + await sql\`SELECT 1\`; + } catch (err) { + if (/connection must be a PostgresSQLConnection/i.test(err && err.message)) { + corrupted = true; + break; + } + } +} + +console.log(JSON.stringify({ corrupted, iterations })); +// A corrupted pool can refuse to close cleanly (sql.close() spins trying to +// flush ghost entries), so just exit immediately — the subprocess dying is +// the tear-down signal for the fake server. +process.exit(0); +`; + +test("pool recovers after every connection is closed mid-handshake", async () => { + using dir = tempDir("pg-close-mid-handshake", { + "fixture.js": FIXTURE, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "fixture.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Fixture prints a single JSON line on stdout. `corrupted: true` means the + // pool produced the internal "connection must be a PostgresSQLConnection" + // error at least once — always a bug, never expected fallout from the test + // scenario (the fake server just closes every connection). + // Fold stderr into the assertion so a fixture crash surfaces in the diff + // instead of leaving CI with an opaque `corrupted: undefined`. + const line = stdout.trim().split("\n").at(-1) ?? ""; + const parsed = line ? JSON.parse(line) : {}; + expect({ stderr, corrupted: parsed.corrupted, exitCode }).toEqual({ + stderr: "", + corrupted: false, + exitCode: 0, + }); +});