From a1523d7bdb4a09a940f2746f2dead9be842234d2 Mon Sep 17 00:00:00 2001 From: Anurag Goel Date: Thu, 24 Sep 2026 08:54:09 -0700 Subject: [PATCH] Log the error when Postgres closes an idle pool connection Postgres can close an idle connection of the pg pool, for example in a restart, a failover, or maintenance, or with pg_terminate_backend. Then pg-pool emits "error" on the pool. db() attached no listener, so Node stopped the process with "Unhandled 'error' event" (57P01). The gateway and the workflows host both use this pool, so a restart of Postgres stopped both. db() now attaches an "error" listener that logs the error and does not throw it. The pool removes the client before it emits the event, and the next query gets a new connection. tests/store.test.ts emits "error" on the pool. Without the listener, the emit throws. The test connects to no Postgres. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 4 ++-- app/store.ts | 7 +++++++ tests/store.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/store.test.ts diff --git a/AGENTS.md b/AGENTS.md index 01b25e9..0c75753 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,8 +175,8 @@ templates/ fullstack/ web/ (Vite + React + Tailwind + shadcn/ui), api/ (Hono + pg) scripts/ migrate, doctor, demo, support tests/ agents, blueprint, contracts, gateway, git, github-auth, - host, images, policy, render, shell, teardown, templates, - tools, workflow + host, images, policy, render, shell, store, teardown, + templates, tools, workflow ``` There is no `tasks.ts`, `scaffold.ts`, `shell.ts`, `github.ts`, or `format.ts`: diff --git a/app/store.ts b/app/store.ts index 8d33104..c8592b8 100644 --- a/app/store.ts +++ b/app/store.ts @@ -111,6 +111,13 @@ export function db(): pg.Pool { allowExitOnIdle: true, options: "-c statement_timeout=15000 -c lock_timeout=5000", }); + // Postgres can close an idle connection of the pool, for example in a + // restart or a failover. The pool then removes the client and emits + // "error". If no listener gets the event, Node stops the process. The + // next query gets a new connection, so the listener only logs the error. + pool.on("error", (error) => { + console.error("Lost an idle Postgres connection:", error); + }); } return pool; } diff --git a/tests/store.test.ts b/tests/store.test.ts new file mode 100644 index 0000000..abf5ed7 --- /dev/null +++ b/tests/store.test.ts @@ -0,0 +1,29 @@ +/** + * Postgres can close an idle connection of the pool, for example in a + * restart. The pool then emits "error", and an "error" event with no listener + * stops the gateway and the workflows host. No test connects to Postgres: the + * pool opens a connection only for a query. + */ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { db } from "../app/store.js"; + +describe("db", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + it("logs the error of an idle connection and does not throw it", () => { + vi.stubEnv("DATABASE_URL", "postgres://factory@127.0.0.1:5432/factory"); + const logged = vi.spyOn(console, "error").mockImplementation(() => {}); + const error = new Error( + "terminating connection due to administrator command", + ); + + expect(() => db().emit("error", error)).not.toThrow(); + expect(logged).toHaveBeenCalledWith( + "Lost an idle Postgres connection:", + error, + ); + }); +});