Skip to content

Commit e795d17

Browse files
author
Kevin Burke
committed
Add option to time out pg.connect() call
Currently if you call pg.connect(), the call will block indefinitely until a connection becomes available. In many cases, if a connection is not available after some period of time, it's preferable to return an error (and call control) to the client, instead of tying up resources forever. Blocking on resource checkout also makes it easier for clients to deadlock - recently at Shyp, we had a situation where a row got locked and the thread that could unlock it was blocked waiting for a connection to become available, leading to deadlock. In that situation, it would be better to abort the checkout, which would have errored, but also broken the deadlock. Add a new setting to defaults: `acquireTimeout`, which will wait for `acquireTimeout` milliseconds before giving up and returning an error. If the value is undefined (the default), `node-postgres` will continue to wait indefinitely for a connection to become available. This builds on a pull request against `generic-pool`, support options.timeout: coopernurse/node-pool#127. Review has been slow going, so I published a new package with that change as `generic-pool-timeout`, and updated the reference in this codebase. Adds semicolons in many places that omitted them and fixes several typos. I'm happy to pull those out into a different commit. Sets the TZ=GMT environment variable before running the tests; without this value set, and with a Postgres server set to the America/Los_Angeles timezone, a timezone test failed. Fixes brianc#782 and brianc#805. Will help alleviate brianc#902. May help with brianc#397.
1 parent 55abbaa commit e795d17

File tree

10 files changed

+99
-28
lines changed

10 files changed

+99
-28
lines changed

Makefile

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ connectionString=postgres://
44

55
params := $(connectionString)
66

7-
node-command := xargs -n 1 -I file node file $(params)
7+
node-command := TZ=GMT xargs -n 1 -I file node file $(params)
88

99
.PHONY : test test-connection test-integration bench test-native \
10-
jshint publish test-missing-native update-npm
10+
jshint publish test-missing-native update-npm clean
1111

1212
all:
1313
npm install
@@ -67,3 +67,6 @@ prepare-test-db:
6767
jshint:
6868
@echo "***Starting jshint***"
6969
@./node_modules/.bin/jshint lib
70+
71+
clean:
72+
@rm -rf node_modules

lib/defaults.js

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ var defaults = module.exports = {
3030
//from the pool and destroyed
3131
poolIdleTimeout: 30000,
3232

33+
// acquireTimeout is the amount of time in ms to wait to check out a database
34+
// connection. By default, node-postgres will wait indefinitely for a
35+
// connection to become available. If the acquireTimeout is negative,
36+
// or NaN, pg.connect will return an error immediately. If the acquireTimeout
37+
// is zero, node-postgres will return an error in the next tick if the pool
38+
// is full.
39+
//
40+
// The error message on timeout will be "Cannot acquire resource because the
41+
// pool is full".
42+
acquireTimeout: undefined,
43+
3344
//frequency to check for idle clients within the client pool
3445
reapIntervalMillis: 1000,
3546

lib/pool.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var EventEmitter = require('events').EventEmitter;
22

33
var defaults = require('./defaults');
4-
var genericPool = require('generic-pool');
4+
var genericPool = require('generic-pool-timeout');
55

66

77
module.exports = function(Client) {
@@ -72,6 +72,17 @@ module.exports = function(Client) {
7272
pool[key] = EventEmitter.prototype[key];
7373
}
7474
}
75+
var block = true;
76+
if (defaults.block === false || clientConfig.block === false) {
77+
block = false;
78+
}
79+
var timeout;
80+
if (typeof defaults.acquireTimeout === "number" && !isNaN(defaults.acquireTimeout)) {
81+
timeout = defaults.acquireTimeout;
82+
} else if (typeof clientConfig.acquireTimeout === "number" && !isNaN(clientConfig.acquireTimeout)) {
83+
timeout = clientConfig.acquireTimeout;
84+
}
85+
7586
//monkey-patch with connect method
7687
pool.connect = function(cb) {
7788
var domain = process.domain;
@@ -88,10 +99,10 @@ module.exports = function(Client) {
8899
pool.release(client);
89100
}
90101
});
91-
});
102+
}, 0, {timeout: timeout});
92103
};
93104
return pool;
94-
}
105+
},
95106
};
96107

97108
return pools;

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"main": "./lib",
2020
"dependencies": {
2121
"buffer-writer": "1.0.1",
22-
"generic-pool": "2.1.1",
22+
"generic-pool-timeout": "2.7.1",
2323
"packet-reader": "0.2.0",
2424
"pg-connection-string": "0.1.3",
2525
"pg-types": "1.*",

script/test-connection.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pg.connect(helper.config, function(err, client, done) {
99
console.error(err);
1010
process.exit(255);
1111
}
12-
console.log("Checking for existance of required test table 'person'")
12+
console.log("Checking for existence of required test table 'person'")
1313
client.query("SELECT COUNT(name) FROM person", function(err, callback) {
1414
if(err != null) {
1515
console.error("Recieved error when executing query 'SELECT COUNT(name) FROM person'")

test/integration/connection-pool/error-tests.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pg.connect(helper.config, assert.success(function(client, done) {
1717
var params = ['idle'];
1818
if(!isGreater) {
1919
killIdleQuery = 'SELECT procpid, (SELECT pg_terminate_backend(procpid)) AS killed FROM pg_stat_activity WHERE current_query LIKE $1';
20-
params = ['%IDLE%']
20+
params = ['%IDLE%'];
2121
}
2222

2323
//subscribe to the pg error event
@@ -37,5 +37,5 @@ pg.connect(helper.config, assert.success(function(client, done) {
3737
}));
3838
}));
3939

40-
})
40+
});
4141
}));

test/test-helper.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ assert = require('assert');
33

44
var EventEmitter = require('events').EventEmitter;
55
var sys = require('util');
6-
var BufferList = require(__dirname+'/buffer-list')
6+
var BufferList = require(__dirname+'/buffer-list');
77

88
var Connection = require(__dirname + '/../lib/connection');
99

@@ -160,7 +160,7 @@ var expect = function(callback, timeout) {
160160
callback.apply(this, arguments)
161161
}
162162
} else {
163-
throw new Error("Unsupported arrity " + callback.length);
163+
throw new Error("Unsupported arity " + callback.length);
164164
}
165165

166166
}
@@ -193,7 +193,7 @@ process.on('exit', function() {
193193
})
194194

195195
process.on('uncaughtException', function(err) {
196-
console.error("\n %s", err.stack || err.toString())
196+
console.error("\n %s", err.stack || err.toString());
197197
//causes xargs to abort right away
198198
process.exit(255);
199199
});
@@ -248,5 +248,3 @@ module.exports = {
248248
setTimezoneOffset: setTimezoneOffset,
249249
resetTimezoneOffset: resetTimezoneOffset
250250
};
251-
252-

test/unit/client/query-queue-tests.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ test('drain', function() {
4646
test("emits drain", function() {
4747
process.nextTick(function() {
4848
assert.ok(raisedDrain);
49-
})
49+
});
5050
});
5151
});
5252
});

test/unit/pool/basic-tests.js

+58-10
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,30 @@ var libDir = __dirname + '/../../../lib';
55
var poolsFactory = require(libDir + '/pool')
66
var defaults = require(libDir + '/defaults');
77
var poolId = 0;
8+
var pg = require(libDir);
89

910
require(__dirname + '/../../test-helper');
1011

1112
var FakeClient = function() {
1213
EventEmitter.call(this);
13-
}
14+
};
1415

1516
util.inherits(FakeClient, EventEmitter);
1617

1718
FakeClient.prototype.connect = function(cb) {
1819
process.nextTick(cb);
19-
}
20+
};
2021

2122
FakeClient.prototype.end = function() {
2223
this.endCalled = true;
23-
}
24+
};
2425
var pools = poolsFactory(FakeClient);
2526

2627
//Hangs the event loop until 'end' is called on client
2728
var HangingClient = function(config) {
2829
EventEmitter.call(this);
2930
this.config = config;
30-
}
31+
};
3132

3233
util.inherits(HangingClient, EventEmitter);
3334

@@ -36,11 +37,11 @@ HangingClient.prototype.connect = function(cb) {
3637
console.log('hung client...');
3738
}, 1000);
3839
process.nextTick(cb);
39-
}
40+
};
4041

4142
HangingClient.prototype.end = function() {
4243
clearInterval(this.intervalId);
43-
}
44+
};
4445

4546
test('no pools exist', function() {
4647
assert.empty(Object.keys(pools.all));
@@ -153,7 +154,7 @@ test('pool with connection error on connection', function() {
153154
});
154155
});
155156

156-
test('returnning an error to done()', function() {
157+
test('returning an error to done()', function() {
157158
var p = pools.getOrCreate(poolId++);
158159
p.connect(function(err, client, done) {
159160
assert.equal(err, null);
@@ -178,7 +179,6 @@ test('fetching pool by object', function() {
178179
assert.equal(p, p2);
179180
});
180181

181-
182182
test('pool#connect client.poolCount', function() {
183183
var p = pools.getOrCreate(poolId++);
184184
var tid;
@@ -187,7 +187,7 @@ test('pool#connect client.poolCount', function() {
187187
tid = setTimeout(function() {
188188
throw new Error("Connection callback was never called");
189189
}, 100);
190-
}
190+
};
191191

192192
setConnectTimeout();
193193
p.connect(function(err, client, done) {
@@ -209,6 +209,54 @@ test('pool#connect client.poolCount', function() {
209209
assert.equal(client.poolCount, undefined,
210210
'after pool is destroyed, count should be undefined');
211211
});
212-
})
212+
});
213+
});
214+
});
215+
216+
pg.defaults.poolSize = 1;
217+
218+
test('pool#connect acquire errors if the pool is full', function() {
219+
var p = pools.getOrCreate(poolId++);
220+
p.connect(function(err, client, done1) {
221+
p.connect(function(err, client, done2) {
222+
assert.equal(err.message, "Cannot acquire resource because the pool is full");
223+
});
224+
});
225+
});
226+
227+
pg.defaults.poolSize = 1;
228+
229+
pg.defaults.poolSize = 1;
230+
pg.defaults.acquireTimeout = 10;
231+
pg.defaults.poolIdleTimeout = 10;
232+
233+
test('pool#connect acquire errors if acquisition times out', function() {
234+
var p = pools.getOrCreate(poolId++);
235+
p.connect(function(err, client, done1) {
236+
setTimeout(function() {
237+
done1();
238+
}, 40);
239+
240+
var start = Date.now();
241+
p.connect(function(err, client, done2) {
242+
assert.equal(err.message, "Cannot acquire resource because the pool is full");
243+
assert.ok((Date.now() - start) < 15);
244+
});
245+
});
246+
});
247+
248+
pg.defaults.acquireTimeout = 20;
249+
250+
test('pool#connect acquire returns a resource if one becomes available before the timeout', function() {
251+
var p = pools.getOrCreate(poolId++);
252+
p.connect(function(err, client, done1) {
253+
setTimeout(function() {
254+
done1();
255+
}, 10);
256+
257+
p.connect(function(err, client, done2) {
258+
assert.equal(err, null);
259+
done2();
260+
});
213261
});
214262
});

test/unit/pool/timeout-tests.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ require(__dirname + '/../../test-helper');
1010

1111
var FakeClient = function() {
1212
EventEmitter.call(this);
13-
}
13+
};
1414

1515
util.inherits(FakeClient, EventEmitter);
1616

1717
FakeClient.prototype.connect = function(cb) {
1818
process.nextTick(cb);
19-
}
19+
};
2020

2121
FakeClient.prototype.end = function() {
2222
this.endCalled = true;
23-
}
23+
};
2424

2525
defaults.poolIdleTimeout = 10;
2626
defaults.reapIntervalMillis = 10;

0 commit comments

Comments
 (0)