A lost idle Postgres connection no longer kills Heimdall

Soul pass 3 (R2): the pool had no 'error' listener. pg emits an idle
client's connection loss there (a database restart, a terminated backend),
so the event went unheard, the process died, and Node printed the pg Client
with its connection parameters.

createPostgresStore now listens and writes one line naming the error code.
The pool has already dropped the dead client and connects again on the
next query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 15:30:15 +02:00
commit c626a86d55
2 changed files with 39 additions and 1 deletions

View file

@ -800,5 +800,17 @@ export function createPostgresStore(databaseUrl: string): PostgresStore {
const pool = new Pool({
connectionString: databaseUrl,
});
// An idle client whose connection ends (a database restart, an
// administrator terminating the backend) is emitted here. Unheard, the
// event kills the process and Node prints the pg Client, connection
// parameters included. The pool has already dropped the dead client and
// opens a new one on the next query, so this only says what happened, by
// code.
pool.on("error", (error) => {
const code = (error as { code?: unknown }).code;
console.error(
`Heimdall lost an idle Postgres connection (${typeof code === "string" ? code : "error"}); the pool reconnects on the next query.`
);
});
return new PostgresStore(pool);
}

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { newDb } from "pg-mem";
import { PostgresStore } from "../src/store/postgres.js";
import { createPostgresStore, PostgresStore } from "../src/store/postgres.js";
describe("PostgresStore", () => {
it("round-trips core auth records through postgres", async () => {
@ -183,3 +183,29 @@ describe("PostgresStore", () => {
await store.close();
});
});
describe("createPostgresStore", () => {
// pg emits an idle client's connection loss on the pool. With no listener
// that event kills the process, and Node prints the pg Client with its
// connection parameters.
it("survives an idle connection's loss and says so by code alone", async () => {
const store = createPostgresStore("postgres://heimdall:CANARYpw@127.0.0.1:1/heimdall");
const pool = (store as unknown as { pool: { emit(event: string, ...args: unknown[]): boolean } }).pool;
const logged: string[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => void logged.push(args.map(String).join(" "));
try {
const terminated = Object.assign(new Error("terminating connection due to administrator command CANARY"), {
code: "57P01",
client: { connectionParameters: { password: "CANARYpw" } },
});
expect(() => pool.emit("error", terminated, terminated.client)).not.toThrow();
} finally {
console.error = originalError;
await store.close();
}
expect(logged).toEqual([
"Heimdall lost an idle Postgres connection (57P01); the pool reconnects on the next query.",
]);
});
});