From 0d9e1fd961cc0a47d20d3823b949956d604e39d8 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 16:08:41 -0600 Subject: [PATCH 1/6] wip --- packages/pg-pool/index.js | 4 ++++ packages/pg-pool/test/lifecyclie-hooks.js | 24 +++++++++++++++++++++++ packages/pg-pool/test/timeout.js | 0 3 files changed, 28 insertions(+) create mode 100644 packages/pg-pool/test/lifecyclie-hooks.js delete mode 100644 packages/pg-pool/test/timeout.js diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index f53a85ab1..c064f02fb 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -277,6 +277,10 @@ class Pool extends EventEmitter { } else { this.log('new client connected') + if (this.options.hooks && this.options.hooks.connect) { + this.options.hooks.connect(client) + } + if (this.options.maxLifetimeSeconds !== 0) { const maxLifetimeTimeout = setTimeout(() => { this.log('ending client due to expired lifetime') diff --git a/packages/pg-pool/test/lifecyclie-hooks.js b/packages/pg-pool/test/lifecyclie-hooks.js new file mode 100644 index 000000000..13907750c --- /dev/null +++ b/packages/pg-pool/test/lifecyclie-hooks.js @@ -0,0 +1,24 @@ +const describe = require('mocha').describe +const it = require('mocha').it +const expect = require('expect.js') + +const Pool = require('..') + +describe('lifecycle hooks', () => { + it('are called on connect', async () => { + const pool = new Pool({ + hooks: { + connect: (client) => { + client.HOOK_CONNECT_COUNT = (client.HOOK_CONNECT_COUNT || 0) + 1 + }, + }, + }) + const client = await pool.connect() + expect(client.HOOK_CONNECT_COUNT).to.equal(1) + client.release() + const client2 = await pool.connect() + expect(client).to.equal(client2) + expect(client2.HOOK_CONNECT_COUNT).to.equal(1) + await pool.end() + }) +}) diff --git a/packages/pg-pool/test/timeout.js b/packages/pg-pool/test/timeout.js deleted file mode 100644 index e69de29bb..000000000 From d9a83be184f6db8a34c364687ababb400951d887 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 16:18:02 -0600 Subject: [PATCH 2/6] Initial connect lifecycle working --- packages/pg-pool/index.js | 63 ++++++++++++------- ...lifecyclie-hooks.js => lifecycle-hooks.js} | 1 + 2 files changed, 43 insertions(+), 21 deletions(-) rename packages/pg-pool/test/{lifecyclie-hooks.js => lifecycle-hooks.js} (96%) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index c064f02fb..8b15843dc 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -278,33 +278,54 @@ class Pool extends EventEmitter { this.log('new client connected') if (this.options.hooks && this.options.hooks.connect) { - this.options.hooks.connect(client) - } - - if (this.options.maxLifetimeSeconds !== 0) { - const maxLifetimeTimeout = setTimeout(() => { - this.log('ending client due to expired lifetime') - this._expired.add(client) - const idleIndex = this._idle.findIndex((idleItem) => idleItem.client === client) - if (idleIndex !== -1) { - this._acquireClient( - client, - new PendingItem((err, client, clientRelease) => clientRelease()), - idleListener, - false - ) - } - }, this.options.maxLifetimeSeconds * 1000) - - maxLifetimeTimeout.unref() - client.once('end', () => clearTimeout(maxLifetimeTimeout)) + const hookResult = this.options.hooks.connect(client) + if (hookResult && typeof hookResult.then === 'function') { + hookResult.then( + () => { + this._afterConnect(client, pendingItem, idleListener) + }, + (hookErr) => { + this._clients = this._clients.filter((c) => c !== client) + client.end(() => { + this._pulseQueue() + if (!pendingItem.timedOut) { + pendingItem.callback(hookErr, undefined, NOOP) + } + }) + } + ) + return + } } - return this._acquireClient(client, pendingItem, idleListener, true) + return this._afterConnect(client, pendingItem, idleListener) } }) } + _afterConnect(client, pendingItem, idleListener) { + if (this.options.maxLifetimeSeconds !== 0) { + const maxLifetimeTimeout = setTimeout(() => { + this.log('ending client due to expired lifetime') + this._expired.add(client) + const idleIndex = this._idle.findIndex((idleItem) => idleItem.client === client) + if (idleIndex !== -1) { + this._acquireClient( + client, + new PendingItem((err, client, clientRelease) => clientRelease()), + idleListener, + false + ) + } + }, this.options.maxLifetimeSeconds * 1000) + + maxLifetimeTimeout.unref() + client.once('end', () => clearTimeout(maxLifetimeTimeout)) + } + + return this._acquireClient(client, pendingItem, idleListener, true) + } + // acquire a client for a pending work item _acquireClient(client, pendingItem, idleListener, isNew) { if (isNew) { diff --git a/packages/pg-pool/test/lifecyclie-hooks.js b/packages/pg-pool/test/lifecycle-hooks.js similarity index 96% rename from packages/pg-pool/test/lifecyclie-hooks.js rename to packages/pg-pool/test/lifecycle-hooks.js index 13907750c..9d4c9c395 100644 --- a/packages/pg-pool/test/lifecyclie-hooks.js +++ b/packages/pg-pool/test/lifecycle-hooks.js @@ -19,6 +19,7 @@ describe('lifecycle hooks', () => { const client2 = await pool.connect() expect(client).to.equal(client2) expect(client2.HOOK_CONNECT_COUNT).to.equal(1) + client.release() await pool.end() }) }) From 001771fe612d710456618855d56b18652542eae3 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 16:26:59 -0600 Subject: [PATCH 3/6] Connect hook working --- packages/pg-pool/index.js | 14 +++++- packages/pg-pool/test/lifecycle-hooks.js | 55 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index 8b15843dc..acf85e844 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -278,7 +278,19 @@ class Pool extends EventEmitter { this.log('new client connected') if (this.options.hooks && this.options.hooks.connect) { - const hookResult = this.options.hooks.connect(client) + let hookResult + try { + hookResult = this.options.hooks.connect(client) + } catch (hookErr) { + this._clients = this._clients.filter((c) => c !== client) + client.end(() => { + this._pulseQueue() + if (!pendingItem.timedOut) { + pendingItem.callback(hookErr, undefined, NOOP) + } + }) + return + } if (hookResult && typeof hookResult.then === 'function') { hookResult.then( () => { diff --git a/packages/pg-pool/test/lifecycle-hooks.js b/packages/pg-pool/test/lifecycle-hooks.js index 9d4c9c395..5b054fac4 100644 --- a/packages/pg-pool/test/lifecycle-hooks.js +++ b/packages/pg-pool/test/lifecycle-hooks.js @@ -22,4 +22,59 @@ describe('lifecycle hooks', () => { client.release() await pool.end() }) + + it('are called on connect with an async hook', async () => { + const pool = new Pool({ + hooks: { + connect: async (client) => { + const res = await client.query('SELECT 1 AS num') + client.HOOK_CONNECT_RESULT = res.rows[0].num + }, + }, + }) + const client = await pool.connect() + expect(client.HOOK_CONNECT_RESULT).to.equal(1) + const res = await client.query('SELECT 1 AS num') + expect(res.rows[0].num).to.equal(1) + client.release() + const client2 = await pool.connect() + expect(client).to.equal(client2) + expect(client2.HOOK_CONNECT_RESULT).to.equal(1) + client.release() + await pool.end() + }) + + it('errors out the connect call if the async connect hook rejects', async () => { + const pool = new Pool({ + hooks: { + connect: async (client) => { + await client.query('SELECT INVALID HERE') + }, + }, + }) + try { + await pool.connect() + throw new Error('Expected connect to throw') + } catch (err) { + expect(err.message).to.contain('invalid') + } + await pool.end() + }) + + it('errors out the connect call if the connect hook throws', async () => { + const pool = new Pool({ + hooks: { + connect: () => { + throw new Error('connect hook error') + }, + }, + }) + try { + await pool.connect() + throw new Error('Expected connect to throw') + } catch (err) { + expect(err.message).to.equal('connect hook error') + } + await pool.end() + }) }) From a850c5c408037fcf385000035c3b603cbde31b4f Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 16:54:50 -0600 Subject: [PATCH 4/6] Rename 'hooks.bla' to 'onBla' --- packages/pg-pool/index.js | 4 ++-- packages/pg-pool/test/lifecycle-hooks.js | 26 ++++++++---------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/pg-pool/index.js b/packages/pg-pool/index.js index acf85e844..0d54be906 100644 --- a/packages/pg-pool/index.js +++ b/packages/pg-pool/index.js @@ -277,10 +277,10 @@ class Pool extends EventEmitter { } else { this.log('new client connected') - if (this.options.hooks && this.options.hooks.connect) { + if (this.options.onConnect) { let hookResult try { - hookResult = this.options.hooks.connect(client) + hookResult = this.options.onConnect(client) } catch (hookErr) { this._clients = this._clients.filter((c) => c !== client) client.end(() => { diff --git a/packages/pg-pool/test/lifecycle-hooks.js b/packages/pg-pool/test/lifecycle-hooks.js index 5b054fac4..815851e7c 100644 --- a/packages/pg-pool/test/lifecycle-hooks.js +++ b/packages/pg-pool/test/lifecycle-hooks.js @@ -7,10 +7,8 @@ const Pool = require('..') describe('lifecycle hooks', () => { it('are called on connect', async () => { const pool = new Pool({ - hooks: { - connect: (client) => { - client.HOOK_CONNECT_COUNT = (client.HOOK_CONNECT_COUNT || 0) + 1 - }, + onConnect: (client) => { + client.HOOK_CONNECT_COUNT = (client.HOOK_CONNECT_COUNT || 0) + 1 }, }) const client = await pool.connect() @@ -25,11 +23,9 @@ describe('lifecycle hooks', () => { it('are called on connect with an async hook', async () => { const pool = new Pool({ - hooks: { - connect: async (client) => { - const res = await client.query('SELECT 1 AS num') - client.HOOK_CONNECT_RESULT = res.rows[0].num - }, + onConnect: async (client) => { + const res = await client.query('SELECT 1 AS num') + client.HOOK_CONNECT_RESULT = res.rows[0].num }, }) const client = await pool.connect() @@ -46,10 +42,8 @@ describe('lifecycle hooks', () => { it('errors out the connect call if the async connect hook rejects', async () => { const pool = new Pool({ - hooks: { - connect: async (client) => { - await client.query('SELECT INVALID HERE') - }, + onConnect: async (client) => { + await client.query('SELECT INVALID HERE') }, }) try { @@ -63,10 +57,8 @@ describe('lifecycle hooks', () => { it('errors out the connect call if the connect hook throws', async () => { const pool = new Pool({ - hooks: { - connect: () => { - throw new Error('connect hook error') - }, + onConnect: () => { + throw new Error('connect hook error') }, }) try { From f73387520eaa8a34ca25f758084029eb24e073a6 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 17:02:55 -0600 Subject: [PATCH 5/6] Add more tests --- packages/pg-pool/test/lifecycle-hooks.js | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/pg-pool/test/lifecycle-hooks.js b/packages/pg-pool/test/lifecycle-hooks.js index 815851e7c..a9d843015 100644 --- a/packages/pg-pool/test/lifecycle-hooks.js +++ b/packages/pg-pool/test/lifecycle-hooks.js @@ -55,6 +55,62 @@ describe('lifecycle hooks', () => { await pool.end() }) + it('calls onConnect when using pool.query', async () => { + const pool = new Pool({ + onConnect: async (client) => { + const res = await client.query('SELECT 1 AS num') + client.HOOK_CONNECT_RESULT = res.rows[0].num + }, + }) + const res = await pool.query('SELECT $1::text AS name', ['brianc']) + expect(res.rows[0].name).to.equal('brianc') + const client = await pool.connect() + expect(client.HOOK_CONNECT_RESULT).to.equal(1) + client.release() + await pool.end() + }) + + it('recovers after a hook error', async () => { + let shouldError = true + const pool = new Pool({ + onConnect: () => { + if (shouldError) { + throw new Error('connect hook error') + } + }, + }) + try { + await pool.connect() + throw new Error('Expected connect to throw') + } catch (err) { + expect(err.message).to.equal('connect hook error') + } + shouldError = false + const client = await pool.connect() + const res = await client.query('SELECT 1 AS num') + expect(res.rows[0].num).to.equal(1) + client.release() + await pool.end() + }) + + it('calls onConnect for each new client', async () => { + let connectCount = 0 + const pool = new Pool({ + max: 2, + onConnect: async (client) => { + connectCount++ + await client.query('SELECT 1') + }, + }) + const client1 = await pool.connect() + const client2 = await pool.connect() + expect(connectCount).to.equal(2) + expect(client1).to.not.equal(client2) + client1.release() + client2.release() + await pool.end() + }) + it('errors out the connect call if the connect hook throws', async () => { const pool = new Pool({ onConnect: () => { From ca8c336de7a9c04f2b77cb9ae930304d65b80e2a Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 2 Mar 2026 18:25:15 -0600 Subject: [PATCH 6/6] More cleanup testing --- packages/pg-pool/test/lifecycle-hooks.js | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/pg-pool/test/lifecycle-hooks.js b/packages/pg-pool/test/lifecycle-hooks.js index a9d843015..05a706d95 100644 --- a/packages/pg-pool/test/lifecycle-hooks.js +++ b/packages/pg-pool/test/lifecycle-hooks.js @@ -111,6 +111,42 @@ describe('lifecycle hooks', () => { await pool.end() }) + it('cleans up clients after repeated hook failures', async () => { + let errorCount = 0 + const pool = new Pool({ + max: 2, + onConnect: () => { + if (errorCount < 10) { + errorCount++ + throw new Error('connect hook error') + } + }, + }) + for (let i = 0; i < 10; i++) { + let threw = false + try { + await pool.connect() + } catch (err) { + threw = true + expect(err.message).to.equal('connect hook error') + } + expect(threw).to.equal(true) + } + expect(errorCount).to.equal(10) + expect(pool.totalCount).to.equal(0) + expect(pool.idleCount).to.equal(0) + const client1 = await pool.connect() + const res1 = await client1.query('SELECT 1 AS num') + expect(res1.rows[0].num).to.equal(1) + const client2 = await pool.connect() + const res2 = await client2.query('SELECT 2 AS num') + expect(res2.rows[0].num).to.equal(2) + expect(pool.totalCount).to.equal(2) + client1.release() + client2.release() + await pool.end() + }) + it('errors out the connect call if the connect hook throws', async () => { const pool = new Pool({ onConnect: () => {