Skip to content

Commit f72010f

Browse files
committed
feat: add log and more tests
1 parent eceec55 commit f72010f

3 files changed

Lines changed: 146 additions & 7 deletions

File tree

packages/pg-pool/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ class Pool extends EventEmitter {
116116
return this._clients.length > this.options.min
117117
}
118118

119+
_hasActiveTransaction(client) {
120+
return client && (client._txStatus === 'T' || client._txStatus === 'E')
121+
}
122+
119123
_pulseQueue() {
120124
this.log('pulse queue')
121125
if (this.ended) {
@@ -364,13 +368,15 @@ class Pool extends EventEmitter {
364368
this.ending ||
365369
!client._queryable ||
366370
client._ending ||
367-
client._txStatus === 'T' ||
368-
client._txStatus === 'E' ||
371+
this._hasActiveTransaction(client) ||
369372
client._poolUseCount >= this.options.maxUses
370373
) {
371374
if (client._poolUseCount >= this.options.maxUses) {
372375
this.log('remove expended client')
373376
}
377+
if (this._hasActiveTransaction(client)) {
378+
this.log('remove client with leaked transaction')
379+
}
374380

375381
return this._remove(client, this._pulseQueue.bind(this))
376382
}
Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,28 @@ const describe = require('mocha').describe
55
const it = require('mocha').it
66
const Pool = require('..')
77

8-
describe('poison connection pool defense (_txStatus check)', function () {
8+
describe('leaked connection pool guard', function () {
99
it('removes a client with an open transaction on release', async function () {
10-
const pool = new Pool({ max: 1 })
10+
const logMessages = []
11+
const pool = new Pool({
12+
max: 1,
13+
log: (msg) => logMessages.push(msg),
14+
})
1115
const client = await pool.connect()
1216
await client.query('BEGIN')
1317
expect(client._txStatus).to.be('T')
1418

1519
client.release()
1620
expect(pool.totalCount).to.be(0)
1721
expect(pool.idleCount).to.be(0)
22+
expect(logMessages).to.contain('remove client with leaked transaction')
1823

19-
// pool should still work by creating a fresh connection
24+
// pool recovers by creating a fresh connection
2025
const { rows } = await pool.query('SELECT 1 as num')
2126
expect(rows[0].num).to.be(1)
27+
expect(pool.totalCount).to.be(1)
28+
expect(pool.idleCount).to.be(1)
29+
2230
await pool.end()
2331
})
2432

@@ -45,9 +53,12 @@ describe('poison connection pool defense (_txStatus check)', function () {
4553
expect(pool.totalCount).to.be(0)
4654
expect(pool.idleCount).to.be(0)
4755

48-
// pool should still work
56+
// pool recovers by creating a fresh connection
4957
const { rows } = await pool.query('SELECT 1 as num')
5058
expect(rows[0].num).to.be(1)
59+
expect(pool.totalCount).to.be(1)
60+
expect(pool.idleCount).to.be(1)
61+
5162
await pool.end()
5263
})
5364

@@ -57,7 +68,7 @@ describe('poison connection pool defense (_txStatus check)', function () {
5768
const clientB = await pool.connect()
5869
const clientC = await pool.connect()
5970

60-
// Client A: open transaction (poisoned)
71+
// Client A: open transaction (leaked)
6172
await clientA.query('BEGIN')
6273
expect(clientA._txStatus).to.be('T')
6374

@@ -79,4 +90,50 @@ describe('poison connection pool defense (_txStatus check)', function () {
7990
expect(pool.idleCount).to.be(2)
8091
await pool.end()
8192
})
93+
94+
describe('pool.query', function () {
95+
it('removes a client after pool.query leaks transaction via BEGIN', async function () {
96+
const logMessages = []
97+
const pool = new Pool({ max: 1, log: (msg) => logMessages.push(msg) })
98+
99+
await pool.query('BEGIN')
100+
101+
// Client auto-released with txStatus='T', should be removed
102+
expect(pool.totalCount).to.be(0)
103+
expect(pool.idleCount).to.be(0)
104+
expect(logMessages).to.contain('remove client with leaked transaction')
105+
106+
// Verify pool recovers
107+
const { rows } = await pool.query('SELECT 1 as num')
108+
expect(rows[0].num).to.be(1)
109+
expect(pool.totalCount).to.be(1)
110+
expect(pool.idleCount).to.be(1)
111+
112+
await pool.end()
113+
})
114+
115+
it('removes a client after pool.query in failed transaction state', async function () {
116+
const pool = new Pool({ max: 1 })
117+
118+
await pool.query('BEGIN')
119+
120+
try {
121+
await pool.query('SELECT invalid_column FROM nonexistent_table')
122+
} catch (e) {
123+
// Expected error
124+
}
125+
126+
// Client with txStatus='E' should be removed
127+
expect(pool.totalCount).to.be(0)
128+
expect(pool.idleCount).to.be(0)
129+
130+
// Pool recovers
131+
const { rows } = await pool.query('SELECT 1 as num')
132+
expect(rows[0].num).to.be(1)
133+
expect(pool.totalCount).to.be(1)
134+
expect(pool.idleCount).to.be(1)
135+
136+
await pool.end()
137+
})
138+
})
82139
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict'
2+
const helper = require('./test-helper')
3+
const suite = new helper.Suite()
4+
const pg = helper.pg
5+
const assert = require('assert')
6+
7+
suite.test('txStatus tracking', function (done) {
8+
const client = new pg.Client()
9+
client.connect(
10+
assert.success(function () {
11+
// Run a simple query to initialize txStatus
12+
client.query(
13+
'SELECT 1',
14+
assert.success(function () {
15+
// Test 1: Initial state after query (should be idle)
16+
assert.equal(client._txStatus, 'I', 'should start in idle state')
17+
18+
// Test 2: BEGIN transaction
19+
client.query(
20+
'BEGIN',
21+
assert.success(function () {
22+
assert.equal(client._txStatus, 'T', 'should be in transaction state')
23+
24+
// Test 3: COMMIT
25+
client.query(
26+
'COMMIT',
27+
assert.success(function () {
28+
assert.equal(client._txStatus, 'I', 'should return to idle after commit')
29+
30+
client.end(done)
31+
})
32+
)
33+
})
34+
)
35+
})
36+
)
37+
})
38+
)
39+
})
40+
41+
suite.test('txStatus error state', function (done) {
42+
const client = new pg.Client()
43+
client.connect(
44+
assert.success(function () {
45+
// Run a simple query to initialize txStatus
46+
client.query(
47+
'SELECT 1',
48+
assert.success(function () {
49+
client.query(
50+
'BEGIN',
51+
assert.success(function () {
52+
// Execute invalid SQL to trigger error state
53+
client.query('INVALID SQL SYNTAX', function (err) {
54+
assert(err, 'should receive error from invalid query')
55+
56+
// Use setImmediate to allow ReadyForQuery message to be processed
57+
setImmediate(function () {
58+
assert.equal(client._txStatus, 'E', 'should be in error state')
59+
60+
// Rollback to recover
61+
client.query(
62+
'ROLLBACK',
63+
assert.success(function () {
64+
assert.equal(client._txStatus, 'I', 'should return to idle after rollback from error')
65+
client.end(done)
66+
})
67+
)
68+
})
69+
})
70+
})
71+
)
72+
})
73+
)
74+
})
75+
)
76+
})

0 commit comments

Comments
 (0)