diff --git a/src/store/postgres.ts b/src/store/postgres.ts index 3ea5f88..0ae0ea5 100644 --- a/src/store/postgres.ts +++ b/src/store/postgres.ts @@ -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); } diff --git a/tests/store.test.ts b/tests/store.test.ts index e8db032..30d6861 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -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.", + ]); + }); +});