Skip to content

Commit eb076db

Browse files
edevilbrianc
authored andcommitted
Add configurable query timeout (#1760)
* Add read_timeout to connection settings * Fix uncaught error issue * Fix lint * Fix "queryCallback is not a function" * Added test and fixed error returning * Added query timeout to native client * Added test for timeout not reached * Ensure error is the correct one Correct test name * Removed dubious check * Added new test * Improved test
1 parent 3620e23 commit eb076db

File tree

5 files changed

+122
-3
lines changed

5 files changed

+122
-3
lines changed

lib/client.js

+36
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,20 @@ Client.prototype.query = function (config, values, callback) {
399399
// can take in strings, config object or query object
400400
var query
401401
var result
402+
var readTimeout
403+
var readTimeoutTimer
404+
var queryCallback
402405

403406
if (config === null || config === undefined) {
404407
throw new TypeError('Client was passed a null or undefined query')
405408
} else if (typeof config.submit === 'function') {
409+
readTimeout = config.query_timeout || this.connectionParameters.query_timeout
406410
result = query = config
407411
if (typeof values === 'function') {
408412
query.callback = query.callback || values
409413
}
410414
} else {
415+
readTimeout = this.connectionParameters.query_timeout
411416
query = new Query(config, values, callback)
412417
if (!query.callback) {
413418
result = new this._Promise((resolve, reject) => {
@@ -416,6 +421,37 @@ Client.prototype.query = function (config, values, callback) {
416421
}
417422
}
418423

424+
if (readTimeout) {
425+
queryCallback = query.callback
426+
427+
readTimeoutTimer = setTimeout(() => {
428+
var error = new Error('Query read timeout')
429+
430+
process.nextTick(() => {
431+
query.handleError(error, this.connection)
432+
})
433+
434+
queryCallback(error)
435+
436+
// we already returned an error,
437+
// just do nothing if query completes
438+
query.callback = () => {}
439+
440+
// Remove from queue
441+
var index = this.queryQueue.indexOf(query)
442+
if (index > -1) {
443+
this.queryQueue.splice(index, 1)
444+
}
445+
446+
this._pulseQueryQueue()
447+
}, readTimeout)
448+
449+
query.callback = (err, res) => {
450+
clearTimeout(readTimeoutTimer)
451+
queryCallback(err, res)
452+
}
453+
}
454+
419455
if (this.binary && !query.binary) {
420456
query.binary = true
421457
}

lib/connection-parameters.js

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ var ConnectionParameters = function (config) {
6565
this.application_name = val('application_name', config, 'PGAPPNAME')
6666
this.fallback_application_name = val('fallback_application_name', config, false)
6767
this.statement_timeout = val('statement_timeout', config, false)
68+
this.query_timeout = val('query_timeout', config, false)
6869
}
6970

7071
// Convert arg to a string, surround in single quotes, and escape single quotes and backslashes

lib/defaults.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ module.exports = {
5555
parseInputDatesAsUTC: false,
5656

5757
// max milliseconds any query using this connection will execute for before timing out in error. false=unlimited
58-
statement_timeout: false
58+
statement_timeout: false,
59+
60+
// max miliseconds to wait for query to complete (client side)
61+
query_timeout: false
5962
}
6063

6164
var pgTypes = require('pg-types')

lib/native/client.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,21 @@ Client.prototype.connect = function (callback) {
146146
Client.prototype.query = function (config, values, callback) {
147147
var query
148148
var result
149-
150-
if (typeof config.submit === 'function') {
149+
var readTimeout
150+
var readTimeoutTimer
151+
var queryCallback
152+
153+
if (config === null || config === undefined) {
154+
throw new TypeError('Client was passed a null or undefined query')
155+
} else if (typeof config.submit === 'function') {
156+
readTimeout = config.query_timeout || this.connectionParameters.query_timeout
151157
result = query = config
152158
// accept query(new Query(...), (err, res) => { }) style
153159
if (typeof values === 'function') {
154160
config.callback = values
155161
}
156162
} else {
163+
readTimeout = this.connectionParameters.query_timeout
157164
query = new NativeQuery(config, values, callback)
158165
if (!query.callback) {
159166
let resolveOut, rejectOut
@@ -165,6 +172,37 @@ Client.prototype.query = function (config, values, callback) {
165172
}
166173
}
167174

175+
if (readTimeout) {
176+
queryCallback = query.callback
177+
178+
readTimeoutTimer = setTimeout(() => {
179+
var error = new Error('Query read timeout')
180+
181+
process.nextTick(() => {
182+
query.handleError(error, this.connection)
183+
})
184+
185+
queryCallback(error)
186+
187+
// we already returned an error,
188+
// just do nothing if query completes
189+
query.callback = () => {}
190+
191+
// Remove from queue
192+
var index = this._queryQueue.indexOf(query)
193+
if (index > -1) {
194+
this._queryQueue.splice(index, 1)
195+
}
196+
197+
this._pulseQueryQueue()
198+
}, readTimeout)
199+
200+
query.callback = (err, res) => {
201+
clearTimeout(readTimeoutTimer)
202+
queryCallback(err, res)
203+
}
204+
}
205+
168206
if (!this._queryable) {
169207
query.native = this.native
170208
process.nextTick(() => {

test/integration/client/api-tests.js

+41
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,47 @@ suite.test('pool callback behavior', done => {
1515
})
1616
})
1717

18+
suite.test('query timeout', (cb) => {
19+
const pool = new pg.Pool({query_timeout: 1000})
20+
pool.connect().then((client) => {
21+
client.query('SELECT pg_sleep(2)', assert.calls(function (err, result) {
22+
assert(err)
23+
assert(err.message === 'Query read timeout')
24+
client.release()
25+
pool.end(cb)
26+
}))
27+
})
28+
})
29+
30+
suite.test('query recover from timeout', (cb) => {
31+
const pool = new pg.Pool({query_timeout: 1000})
32+
pool.connect().then((client) => {
33+
client.query('SELECT pg_sleep(20)', assert.calls(function (err, result) {
34+
assert(err)
35+
assert(err.message === 'Query read timeout')
36+
client.release(err)
37+
pool.connect().then((client) => {
38+
client.query('SELECT 1', assert.calls(function (err, result) {
39+
assert(!err)
40+
client.release(err)
41+
pool.end(cb)
42+
}))
43+
})
44+
}))
45+
})
46+
})
47+
48+
suite.test('query no timeout', (cb) => {
49+
const pool = new pg.Pool({query_timeout: 10000})
50+
pool.connect().then((client) => {
51+
client.query('SELECT pg_sleep(1)', assert.calls(function (err, result) {
52+
assert(!err)
53+
client.release()
54+
pool.end(cb)
55+
}))
56+
})
57+
})
58+
1859
suite.test('callback API', done => {
1960
const client = new helper.Client()
2061
client.query('CREATE TEMP TABLE peep(name text)')

0 commit comments

Comments
 (0)