Skip to content

Commit 53584b7

Browse files
authored
Add connection & query timeout if all clients are checked out (brianc#70)
* Add connection & query timeout if all clients are checked out This addresses [pg#1390](brianc#1390). Ensure connection timeout applies both for new connections and on an exhuasted pool. I also made the library return an error when passing a function as the first param to `pool.query` - previosuly this threw a sync type error. * Add pg-cursor to dev deps
1 parent a446537 commit 53584b7

File tree

5 files changed

+109
-3
lines changed

5 files changed

+109
-3
lines changed

index.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,34 @@ class Pool extends EventEmitter {
128128
const err = new Error('Cannot use a pool after calling end on the pool')
129129
return cb ? cb(err) : this.Promise.reject(err)
130130
}
131+
132+
// if we don't have to connect a new client, don't do so
131133
if (this._clients.length >= this.options.max || this._idle.length) {
132134
const response = promisify(this.Promise, cb)
133135
const result = response.result
134-
this._pendingQueue.push(response.callback)
136+
135137
// if we have idle clients schedule a pulse immediately
136138
if (this._idle.length) {
137139
process.nextTick(() => this._pulseQueue())
138140
}
141+
142+
if (!this.options.connectionTimeoutMillis) {
143+
this._pendingQueue.push(response.callback)
144+
return result
145+
}
146+
147+
// set connection timeout on checking out an existing client
148+
const tid = setTimeout(() => {
149+
// remove the callback from pending waiters because
150+
// we're going to call it with a timeout error
151+
this._pendingQueue = this._pendingQueue.filter(cb => cb === response.callback)
152+
response.callback(new Error('timeout exceeded when trying to connect'))
153+
}, this.options.connectionTimeoutMillis)
154+
155+
this._pendingQueue.push(function (err, res, done) {
156+
clearTimeout(tid)
157+
response.callback(err, res, done)
158+
})
139159
return result
140160
}
141161

@@ -199,6 +219,16 @@ class Pool extends EventEmitter {
199219
}
200220

201221
query (text, values, cb) {
222+
// guard clause against passing a function as the first parameter
223+
if (typeof text === 'function') {
224+
const response = promisify(this.Promise, text)
225+
setImmediate(function () {
226+
return response.callback(new Error('Passing a function as the first parameter to pool.query is not supported'))
227+
})
228+
return response.result
229+
}
230+
231+
// allow plain text query without values
202232
if (typeof values === 'function') {
203233
cb = values
204234
values = undefined

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"test": "test"
88
},
99
"scripts": {
10-
"test": "node_modules/.bin/standard && node_modules/.bin/mocha"
10+
"test": " node_modules/.bin/mocha && node_modules/.bin/standard"
1111
},
1212
"repository": {
1313
"type": "git",
@@ -32,6 +32,7 @@
3232
"lodash": "4.13.1",
3333
"mocha": "^2.3.3",
3434
"pg": "*",
35+
"pg-cursor": "^1.3.0",
3536
"standard": "7.1.2",
3637
"standard-format": "2.2.1"
3738
},

test/connection-timeout.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,50 @@ describe('connection timeout', () => {
5858
}
5959
expect(errors).to.have.length(15)
6060
}.bind(this)))
61-
})
6261

62+
it('should timeout on checkout of used connection', (done) => {
63+
const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
64+
pool.connect((err, client, release) => {
65+
expect(err).to.be(undefined)
66+
expect(client).to.not.be(undefined)
67+
pool.connect((err, client) => {
68+
expect(err).to.be.an(Error)
69+
expect(client).to.be(undefined)
70+
release()
71+
pool.end(done)
72+
})
73+
})
74+
})
75+
76+
it('should timeout on query if all clients are busy', (done) => {
77+
const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
78+
pool.connect((err, client, release) => {
79+
expect(err).to.be(undefined)
80+
expect(client).to.not.be(undefined)
81+
pool.query('select now()', (err, result) => {
82+
expect(err).to.be.an(Error)
83+
expect(result).to.be(undefined)
84+
release()
85+
pool.end(done)
86+
})
87+
})
88+
})
89+
90+
it('should recover from timeout errors', (done) => {
91+
const pool = new Pool({ connectionTimeoutMillis: 100, max: 1 })
92+
pool.connect((err, client, release) => {
93+
expect(err).to.be(undefined)
94+
expect(client).to.not.be(undefined)
95+
pool.query('select now()', (err, result) => {
96+
expect(err).to.be.an(Error)
97+
expect(result).to.be(undefined)
98+
release()
99+
pool.query('select $1::text as name', ['brianc'], (err, res) => {
100+
expect(err).to.be(undefined)
101+
expect(res.rows).to.have.length(1)
102+
pool.end(done)
103+
})
104+
})
105+
})
106+
})
107+
})

test/error-handling.js

+11
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ describe('pool error handling', function () {
112112
}))
113113
})
114114

115+
describe('passing a function to pool.query', () => {
116+
it('calls back with error', (done) => {
117+
const pool = new Pool()
118+
console.log('passing fn to query')
119+
pool.query((err) => {
120+
expect(err).to.be.an(Error)
121+
pool.end(done)
122+
})
123+
})
124+
})
125+
115126
describe('pool with lots of errors', () => {
116127
it('continues to work and provide new clients', co.wrap(function * () {
117128
const pool = new Pool({ max: 1 })

test/submittable.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
const Cursor = require('pg-cursor')
3+
const expect = require('expect.js')
4+
const describe = require('mocha').describe
5+
const it = require('mocha').it
6+
7+
const Pool = require('../')
8+
9+
describe('submittle', () => {
10+
it('is returned from the query method', false, (done) => {
11+
const pool = new Pool()
12+
const cursor = pool.query(new Cursor('SELECT * from generate_series(0, 1000)'))
13+
cursor.read((err, rows) => {
14+
expect(err).to.be(undefined)
15+
expect(!!rows).to.be.ok()
16+
cursor.close(done)
17+
})
18+
})
19+
})

0 commit comments

Comments
 (0)