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, + ); + }); +});