Files

110 lines
4.2 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
// Fixture for postgres-finish-request-underflow.test.ts. Runs in a subprocess
// so a debug_assert panic on the counter invariant is observable as a nonzero
// exit code instead of taking down the test runner.
//
// Drives a single connection through every finish_request call site:
// - the ReadyForQuery 'Z' arm, for a simple query that completes normally
// - the ErrorResponse 'E' arm, for a simple query the server rejects
// - the ErrorResponse arm followed by CommandComplete + ReadyForQuery for the
// same exchange (the sequence whose second CommandComplete used to flip the
// request back to PartialResponse and double-decrement the counter)
//
// After each exchange a fresh query must still dispatch: if the per-class
// request counter leaked high or wrapped past zero, advance() would refuse to
// write it and the subprocess would sit idle until the watchdog fires.
import { SQL } from "bun";
import {
listeningServer,
pgAuthenticationOk,
pgCommandComplete,
pgErrorResponse,
pgReadFrontendMessages,
pgReadyForQuery,
} from "./wire-frames";
const watchdog = setTimeout(() => {
console.error("WATCHDOG: a later query never dispatched (request counter leaked or wrapped)");
process.exit(1);
}, 15_000);
type ConnState = { buf: Buffer; sawStartup: boolean; simpleCount: number };
const { port, server } = await listeningServer(socket => {
const state: ConnState = { buf: Buffer.alloc(0), sawStartup: false, simpleCount: 0 };
socket.on("data", data => {
state.buf = Buffer.concat([state.buf, data]);
if (!state.sawStartup) {
if (state.buf.length < 4) return;
const len = state.buf.readInt32BE(0);
if (state.buf.length < len) return;
state.buf = state.buf.subarray(len);
state.sawStartup = true;
socket.write(Buffer.concat([pgAuthenticationOk(), pgReadyForQuery()]));
return;
}
state.buf = pgReadFrontendMessages(state.buf, (type, body) => {
if (type !== 0x51 /* 'Q' simple Query */) return;
const q = body.toString("utf8", 0, body.indexOf(0));
state.simpleCount++;
if (q.includes("reject_once")) {
socket.write(
Buffer.concat([pgErrorResponse({ S: "ERROR", C: "XX000", M: "boom" }), pgReadyForQuery()]),
);
} else if (q.includes("reject_then_late_result")) {
// ErrorResponse first, then a late CommandComplete + ReadyForQuery for
// the same exchange. The late CommandComplete must be discarded; the
// ReadyForQuery that follows must not decrement a second time.
socket.write(
Buffer.concat([
pgErrorResponse({ S: "ERROR", C: "XX000", M: "boom" }),
pgCommandComplete("SELECT 0"),
pgReadyForQuery(),
]),
);
} else {
socket.write(
Buffer.concat([pgCommandComplete(`SELECT ${state.simpleCount}`), pgReadyForQuery()]),
);
}
});
});
socket.on("error", () => {});
});
const opts = { url: `postgres://[email protected]:${port}/db`, max: 1, idleTimeout: 5, connectionTimeout: 5 } as const;
// 1. normal simple-query completion: finish_request via the 'Z' arm.
{
const sql = new SQL(opts);
await sql.unsafe("select ok").simple();
await sql.unsafe("select still_dispatches").simple();
await sql.close({ timeout: 0 });
}
// 2. ErrorResponse: finish_request via the 'E' arm, then a follow-up query
// must still dispatch on the same connection.
{
const sql = new SQL(opts);
const err: any = await sql.unsafe("select reject_once").simple().catch(e => e);
if (err?.code !== "ERR_POSTGRES_SERVER_ERROR") {
throw new Error(`expected ERR_POSTGRES_SERVER_ERROR, got ${err?.code ?? err}`);
}
await sql.unsafe("select still_dispatches").simple();
await sql.close({ timeout: 0 });
}
// 3. ErrorResponse + late CommandComplete + ReadyForQuery for the same request.
{
const sql = new SQL(opts);
const err: any = await sql.unsafe("select reject_then_late_result").simple().catch(e => e);
if (err?.code !== "ERR_POSTGRES_SERVER_ERROR") {
throw new Error(`expected ERR_POSTGRES_SERVER_ERROR, got ${err?.code ?? err}`);
}
await sql.unsafe("select still_dispatches").simple();
await sql.close({ timeout: 0 });
}
clearTimeout(watchdog);
await new Promise<void>(resolve => server.close(() => resolve()));
console.log("DONE");