Log the error when Postgres closes a connection in a transaction - #45
Merged
Merged
Conversation
withAppLock() checks out a client from the pool for one transaction. claimRunApp (workflows host) and claimDelete (gateway) use it. While a client is checked out, pg-pool does not listen for "error" on it, and the pool listener of #43 gets only the errors of idle clients. If Postgres closes the connection during the transaction, for example in a restart, a failover, or with pg_terminate_backend, the query fails. Then the socket closes, and the client emits "error" ("Connection terminated unexpectedly"). No listener got the event, so Node stopped the process with exit code 1. withAppLock() now attaches an "error" listener after connect() and removes it before release(). The listener logs the error and does not throw it. The error of the query still goes to the caller, so claimRunApp and claimDelete fail as before. release() attaches the listener of the pool again, and the pool removes the closed client. tests/store.test.ts gives claimDelete a fake client. The lock query fails, and the client emits "error" while the rollback waits. Without the listener, the emit throws. The test connects to no Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
withAppLock()inapp/store.tschecks out a client withdb().connect()for one transaction.claimRunApp(workflows host) andclaimDelete(gateway) use it. While a client is checked out, pg-pool does not listen for"error"on it (see_acquireClientinpg-pool/index.js). The pool listener of #43 gets only the errors of idle clients.If Postgres closes the connection during the transaction, for example in a restart, a failover, maintenance, or with
pg_terminate_backend, the query fails. Then the socket closes, and the pg client emits"error"(Connection terminated unexpectedly). No listener got the event, so Node stopped the process with exit code 1:Change
withAppLock()attaches an"error"listener right afterconnect(). The listener logsLost a Postgres connection in a transaction:withconsole.error, as the pool listener of Log the error when Postgres closes an idle pool connection #43 does, and does not throw the error.finallyblock removes the listener just beforeclient.release().release()attaches the listener of the pool again, and the pool removes the closed client. If the listener remains, each transaction on the same client adds one more.claimRunAppandclaimDeletefail as before.No other code changes.
Test
tests/store.test.tsmakesdb().connect()giveclaimDeletea fake client: anEventEmitterwithqueryandrelease. The lock query fails. Then, in a later macrotask, the client emits"error"while the rollback waits. The test checks that:The test connects to no Postgres. Without the fix, it fails with
expected [Function] to not throw an error but 'Error: Connection terminated unexpectedly' was thrown. Without theoff()line, it fails withexpected 1 to be +0.npm run checkpasses (Biome lint,tsc, 401 tests).End-to-end check
On a throwaway Homebrew Postgres 18 on port 55987, not the docker-compose database. The scripts:
withAppLock():select pg_sleep(5)in a transaction, andpg_terminate_backendfrompsqlafter 1 s. It does not callwithAppLock(), so it can only show the crash.claimDeleteandclaimRunApp. A second session holds the app lock, so the transaction waits inpg_advisory_xact_lock(pg_stat_activityshowsLock:advisory). After 1 s,pg_terminate_backendends thevibe-factorybackend. Postgres sends 57P01, and then the socket closes.SIGKILLon the backend. The connection closes with no error message from Postgres, so the client emits"error"before the query fails.withAppLock())claimDelete,pg_terminate_backend{ claimed: true, runIds: ["run-old"] }.claimRunApp,pg_terminate_backend{ claimed: true }.claimDelete,SIGKILLConnection terminated unexpectedly. After crash recovery, the next claim gives{ claimed: true, runIds: ["run-old"] }.After the fix, each case logs one error of 7 lines: the message and the stack. pg-pool adds
err.clientonly in its idle listener, so this log does not include the settings of the connection.Dependency
#43 is merged. This branch starts from
origin/mainat 2c64c81.🤖 Generated with Claude Code