Skip to content

Commit 4ea20cc

Browse files
author
Kevin Burke
committed
POC: 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 two new settings to defaults: `block`, which, if false, will immediately return an error if the pool is full when you attempt to check out a new connection. Also adds `acquireTimeout`, which will wait for `acquireTimeout` milliseconds before giving up and returning an error. This builds on two pull requests against `generic-pool`: - Support options.block: coopernurse/node-pool#125 - Support options.timeout: coopernurse/node-pool#127 For the moment I'm pointing `generic-pool` at a branch that incorporates both of these commits. I'm marking this as a proof-of-concept until those go through, which hopefully they will soon. I'd also like feedback on the API. 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 dbf3a04 commit 4ea20cc

File tree

10 files changed

+117
-26
lines changed

10 files changed

+117
-26
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

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

33+
// block sets the behavior of node-postgres when the connection pool is
34+
// full. Set to false to immediately return an error if no connections are
35+
// available. By default, node-postgres will wait an infinite amount of time
36+
// for a connection to become available.
37+
block: true,
38+
39+
// acquireTimeout is the amount of time in ms to wait to check out a
40+
// database connection. By default, node-postgres will wait infinitely for
41+
// a connection to become available. If the acquireTimeout is negative, zero,
42+
// or NaN, pg.connect will return an error immediately.
43+
//
44+
// If block is set to false, this setting is ignored - node-postgres will
45+
// error immediately if block is false and the pool is full.
46+
//
47+
// The timeout message will be "Cannot acquire resource because the pool is
48+
// full"
49+
acquireTimeout: undefined,
50+
3351
//frequency to check for idle clients within the client pool
3452
reapIntervalMillis: 1000,
3553

lib/pool.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ var pools = {
7070
pool[key] = EventEmitter.prototype[key];
7171
}
7272
}
73+
var block = true;
74+
if (defaults.block === false || clientConfig.block === false) {
75+
block = false;
76+
}
77+
var timeout;
78+
if (typeof defaults.acquireTimeout === "number" && !isNaN(defaults.acquireTimeout)) {
79+
timeout = defaults.acquireTimeout;
80+
} else if (typeof clientConfig.acquireTimeout === "number" && !isNaN(clientConfig.acquireTimeout)) {
81+
timeout = clientConfig.acquireTimeout;
82+
}
83+
7384
//monkey-patch with connect method
7485
pool.connect = function(cb) {
7586
var domain = process.domain;
@@ -86,7 +97,7 @@ var pools = {
8697
pool.release(client);
8798
}
8899
});
89-
});
100+
}, 0, {block: block, timeout: timeout});
90101
};
91102
return pool;
92103
}

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": "git://github.com/Shyp/node-pool.git#timeout",
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

+71-10
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,30 @@ var libDir = __dirname + '/../../../lib';
55
var defaults = require(libDir + '/defaults');
66
var pools = require(libDir + '/pool');
77
var poolId = 0;
8+
var pg = require(libDir);
9+
pg = pg;
810

911
require(__dirname + '/../../test-helper');
1012

1113
var FakeClient = function() {
1214
EventEmitter.call(this);
13-
}
15+
};
1416

1517
util.inherits(FakeClient, EventEmitter);
1618

1719
FakeClient.prototype.connect = function(cb) {
1820
process.nextTick(cb);
19-
}
21+
};
2022

2123
FakeClient.prototype.end = function() {
2224
this.endCalled = true;
23-
}
25+
};
2426

2527
//Hangs the event loop until 'end' is called on client
2628
var HangingClient = function(config) {
2729
EventEmitter.call(this);
2830
this.config = config;
29-
}
31+
};
3032

3133
util.inherits(HangingClient, EventEmitter);
3234

@@ -35,11 +37,11 @@ HangingClient.prototype.connect = function(cb) {
3537
console.log('hung client...');
3638
}, 1000);
3739
process.nextTick(cb);
38-
}
40+
};
3941

4042
HangingClient.prototype.end = function() {
4143
clearInterval(this.intervalId);
42-
}
44+
};
4345

4446
pools.Client = FakeClient;
4547

@@ -153,7 +155,7 @@ test('pool with connection error on connection', function() {
153155
});
154156
});
155157

156-
test('returnning an error to done()', function() {
158+
test('returning an error to done()', function() {
157159
var p = pools.getOrCreate(poolId++);
158160
pools.Client = FakeClient;
159161
p.connect(function(err, client, done) {
@@ -179,7 +181,6 @@ test('fetching pool by object', function() {
179181
assert.equal(p, p2);
180182
});
181183

182-
183184
test('pool#connect client.poolCount', function() {
184185
var p = pools.getOrCreate(poolId++);
185186
var tid;
@@ -188,7 +189,7 @@ test('pool#connect client.poolCount', function() {
188189
tid = setTimeout(function() {
189190
throw new Error("Connection callback was never called");
190191
}, 100);
191-
}
192+
};
192193

193194
setConnectTimeout();
194195
p.connect(function(err, client, done) {
@@ -210,6 +211,66 @@ test('pool#connect client.poolCount', function() {
210211
assert.equal(client.poolCount, undefined,
211212
'after pool is destroyed, count should be undefined');
212213
});
213-
})
214+
});
215+
});
216+
});
217+
218+
pg.defaults.block = false;
219+
pg.defaults.poolSize = 1;
220+
221+
test('pool#connect acquire errors if the pool is full', function() {
222+
var p = pools.getOrCreate(poolId++);
223+
p.connect(function(err, client, done1) {
224+
p.connect(function(err, client, done2) {
225+
assert.equal(err.message, "Cannot acquire resource because the pool is full");
226+
});
227+
});
228+
});
229+
230+
pg.defaults.block = true;
231+
pg.defaults.poolSize = 1;
232+
233+
test('pool#connect accepts block setting via the clientConfig', function() {
234+
var p = pools.getOrCreate({id: poolId++, block: false});
235+
p.connect(function(err, client, done1) {
236+
p.connect(function(err, client, done2) {
237+
assert.equal(err.message, "Cannot acquire resource because the pool is full");
238+
});
239+
});
240+
});
241+
242+
pg.defaults.block = true;
243+
pg.defaults.poolSize = 1;
244+
pg.defaults.acquireTimeout = 10;
245+
pg.defaults.poolIdleTimeout = 10;
246+
247+
test('pool#connect acquire errors if acquisition times out', function() {
248+
var p = pools.getOrCreate(poolId++);
249+
p.connect(function(err, client, done1) {
250+
setTimeout(function() {
251+
done1();
252+
}, 40);
253+
254+
var start = Date.now();
255+
p.connect(function(err, client, done2) {
256+
assert.equal(err.message, "Cannot acquire resource because the pool is full");
257+
assert.ok((Date.now() - start) < 15);
258+
});
259+
});
260+
});
261+
262+
pg.defaults.acquireTimeout = 20;
263+
264+
test('pool#connect acquire returns a resource if one becomes available before the timeout', function() {
265+
var p = pools.getOrCreate(poolId++);
266+
p.connect(function(err, client, done1) {
267+
setTimeout(function() {
268+
done1();
269+
}, 10);
270+
271+
p.connect(function(err, client, done2) {
272+
assert.equal(err, null);
273+
done2();
274+
});
214275
});
215276
});

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)