initial crack at connection pooling -- still dirty
This commit is contained in:
parent
5a87972983
commit
ab13d0c1eb
100
lib/index.js
100
lib/index.js
@ -1,11 +1,106 @@
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var sys = require('sys');
|
var sys = require('sys');
|
||||||
var net = require('net');
|
var net = require('net');
|
||||||
|
var Pool = require(__dirname + '/utils').Pool;
|
||||||
var Client = require(__dirname+'/client');
|
var Client = require(__dirname+'/client');
|
||||||
|
var defaults = require(__dirname + '/defaults');
|
||||||
|
//connection pool global cache
|
||||||
|
var clientPools = {
|
||||||
|
}
|
||||||
|
|
||||||
|
var poolEnabled = function() {
|
||||||
|
return defaults.poolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = function() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = function() {
|
||||||
|
console.log.apply(console, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
var getPooledClient = function(config, callback) {
|
||||||
|
//lookup pool using config as key
|
||||||
|
//TODO this don't work so hot w/ object configs
|
||||||
|
var pool = clientPools[config];
|
||||||
|
|
||||||
|
//create pool if doesn't exist
|
||||||
|
if(!pool) {
|
||||||
|
log("creating pool %s", config)
|
||||||
|
pool = clientPools[config] = new Pool(defaults.poolSize, function() {
|
||||||
|
log("creating new client in pool %s", config)
|
||||||
|
var client = new Client(config);
|
||||||
|
client.connected = false;
|
||||||
|
return client;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.checkOut(function(err, client) {
|
||||||
|
//if client already connected just
|
||||||
|
//pass it along to the callback and return
|
||||||
|
if(client.connected) {
|
||||||
|
callback(null, client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var onError = function(error) {
|
||||||
|
client.connection.removeListener('readyForQuery', onReady);
|
||||||
|
callback(error);
|
||||||
|
pool.checkIn(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
var onReady = function() {
|
||||||
|
client.removeListener('error', onError);
|
||||||
|
client.connected = true;
|
||||||
|
callback(null, client);
|
||||||
|
client.on('drain', function() {
|
||||||
|
pool.checkIn(client);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.once('error', onError);
|
||||||
|
|
||||||
|
//TODO refactor
|
||||||
|
//i don't like reaching into the client's connection for attaching
|
||||||
|
//to specific events here
|
||||||
|
client.connection.once('readyForQuery', onReady);
|
||||||
|
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//destroys the world
|
||||||
|
var end = function(callback) {
|
||||||
|
for(var name in clientPools) {
|
||||||
|
var pool = clientPools[name];
|
||||||
|
log("destroying pool %s", name);
|
||||||
|
pool.waits.forEach(function(wait) {
|
||||||
|
wait(new Error("Client is being destroyed"))
|
||||||
|
})
|
||||||
|
pool.waits = [];
|
||||||
|
pool.items.forEach(function(item) {
|
||||||
|
var client = item.ref;
|
||||||
|
if(client.activeQuery) {
|
||||||
|
log("client is still active, waiting for it to complete");
|
||||||
|
client.on('drain', client.end.bind(client))
|
||||||
|
} else {
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//remove reference to pool lookup
|
||||||
|
clientPools[name] = null;
|
||||||
|
delete(clientPools[name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//wrap up common connection management boilerplate
|
//wrap up common connection management boilerplate
|
||||||
var connect = function(config, callback) {
|
var connect = function(config, callback) {
|
||||||
|
if(poolEnabled()) {
|
||||||
|
return getPooledClient(config, callback)
|
||||||
|
}
|
||||||
|
throw new Error("FUCK")
|
||||||
var client = new Client(config);
|
var client = new Client(config);
|
||||||
client.connect();
|
client.connect();
|
||||||
|
|
||||||
@ -32,5 +127,6 @@ module.exports = {
|
|||||||
Client: Client,
|
Client: Client,
|
||||||
Connection: require(__dirname + '/connection'),
|
Connection: require(__dirname + '/connection'),
|
||||||
connect: connect,
|
connect: connect,
|
||||||
defaults: require(__dirname + '/defaults')
|
end: end,
|
||||||
|
defaults: defaults
|
||||||
}
|
}
|
||||||
|
27
lib/utils.js
27
lib/utils.js
@ -1,4 +1,5 @@
|
|||||||
var events = require('events');
|
var events = require('events');
|
||||||
|
var sys = require('sys');
|
||||||
|
|
||||||
if(typeof events.EventEmitter.prototype.once !== 'function') {
|
if(typeof events.EventEmitter.prototype.once !== 'function') {
|
||||||
events.EventEmitter.prototype.once = function (type, listener) {
|
events.EventEmitter.prototype.once = function (type, listener) {
|
||||||
@ -10,12 +11,13 @@ if(typeof events.EventEmitter.prototype.once !== 'function') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
var Pool = function(maxSize, createFn) {
|
var Pool = function(maxSize, createFn) {
|
||||||
|
events.EventEmitter.call(this);
|
||||||
this.maxSize = maxSize;
|
this.maxSize = maxSize;
|
||||||
this.createFn = createFn;
|
this.createFn = createFn;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.waits = [];
|
this.waits = [];
|
||||||
}
|
}
|
||||||
|
sys.inherits(Pool, events.EventEmitter);
|
||||||
var p = Pool.prototype;
|
var p = Pool.prototype;
|
||||||
|
|
||||||
p.checkOut = function(callback) {
|
p.checkOut = function(callback) {
|
||||||
@ -27,10 +29,19 @@ p.checkOut = function(callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//check if we can create a new item
|
//check if we can create a new item
|
||||||
if(len < this.maxSize && this.createFn) {
|
if(this.items.length < this.maxSize && this.createFn) {
|
||||||
var item = {ref: this.createFn()}
|
var result = this.createFn();
|
||||||
|
var item = result;
|
||||||
|
//create function can return item conforming to interface
|
||||||
|
//of stored items to allow for create function to create
|
||||||
|
//checked out items
|
||||||
|
if(typeof item.checkedIn == "undefined") {
|
||||||
|
var item = {ref: result, checkedIn: true}
|
||||||
|
}
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
return this._pulse(item, callback)
|
if(item.checkedIn) {
|
||||||
|
return this._pulse(item, callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.waits.push(callback);
|
this.waits.push(callback);
|
||||||
return false; //did not execute sync
|
return false; //did not execute sync
|
||||||
@ -42,17 +53,19 @@ p.checkIn = function(item) {
|
|||||||
var currentItem = this.items[i];
|
var currentItem = this.items[i];
|
||||||
if(currentItem.ref == item) {
|
if(currentItem.ref == item) {
|
||||||
currentItem.checkedIn = true;
|
currentItem.checkedIn = true;
|
||||||
return this._pulse(currentItem);
|
this._pulse(currentItem);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//add new item
|
//add new item
|
||||||
var newItem = {ref: item, checkedIn: true};
|
var newItem = {ref: item, checkedIn: true};
|
||||||
this.items.push(newItem);
|
this.items.push(newItem);
|
||||||
return this._pulse(newItem);
|
this._pulse(newItem);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
p._pulse = function(item, cb) {
|
p._pulse = function(item, cb) {
|
||||||
cb = cb || this.waits.pop()
|
cb = cb || this.waits.shift()
|
||||||
if(cb) {
|
if(cb) {
|
||||||
item.checkedIn = false;
|
item.checkedIn = false;
|
||||||
cb(null, item.ref)
|
cb(null, item.ref)
|
||||||
|
@ -36,7 +36,6 @@ test('api', function() {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('executing nested queries', function() {
|
test('executing nested queries', function() {
|
||||||
pg.connect(helper.args, assert.calls(function(err, client) {
|
pg.connect(helper.args, assert.calls(function(err, client) {
|
||||||
client.query('select now as now from NOW()', assert.calls(function(err, result) {
|
client.query('select now as now from NOW()', assert.calls(function(err, result) {
|
||||||
@ -50,9 +49,11 @@ test('executing nested queries', function() {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
test('raises error if cannot connect', function() {
|
test('raises error if cannot connect', function() {
|
||||||
pg.connect({database:'asdlfkajsdf there is no way this is a real database, right?!'}, assert.calls(function(err, client) {
|
var connectionString = "pg://asdf@seoiasfd:4884/ieieie";
|
||||||
|
pg.connect(connectionString, assert.calls(function(err, client) {
|
||||||
assert.ok(err, 'should have raised an error')
|
assert.ok(err, 'should have raised an error')
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pg.end();
|
||||||
|
32
test/integration/connection-pool/single-connection-tests.js
Normal file
32
test/integration/connection-pool/single-connection-tests.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
var helper = require(__dirname + "/test-helper")
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
helper.pg.defaults.poolSize = 10;
|
||||||
|
test('executes a single pooled connection/query', function() {
|
||||||
|
var args = helper.args;
|
||||||
|
var conString = "pg://"+args.user+":"+args.password+"@"+args.host+":"+args.port+"/"+args.database;
|
||||||
|
var queryCount = 0;
|
||||||
|
helper.pg.connect(conString, assert.calls(function(err, client) {
|
||||||
|
assert.isNull(err);
|
||||||
|
client.query("select * from NOW()", assert.calls(function(err, result) {
|
||||||
|
assert.isNull(err);
|
||||||
|
queryCount++;
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
var id = setTimeout(function() {
|
||||||
|
assert.equal(queryCount, 1)
|
||||||
|
}, 1000)
|
||||||
|
var check = function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
if(queryCount == 1) {
|
||||||
|
clearTimeout(id)
|
||||||
|
helper.pg.end();
|
||||||
|
} else {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
4
test/integration/connection-pool/test-helper.js
Normal file
4
test/integration/connection-pool/test-helper.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
args: require(__dirname + "/../test-helper").args,
|
||||||
|
pg: require(__dirname + "/../../../lib")
|
||||||
|
}
|
@ -86,7 +86,7 @@ var expect = function(callback, timeout) {
|
|||||||
var executed = false;
|
var executed = false;
|
||||||
var id = setTimeout(function() {
|
var id = setTimeout(function() {
|
||||||
assert.ok(executed, "Expected execution of " + callback + " fired");
|
assert.ok(executed, "Expected execution of " + callback + " fired");
|
||||||
}, timeout || 1000)
|
}, timeout || 2000)
|
||||||
|
|
||||||
return function(err, queryResult) {
|
return function(err, queryResult) {
|
||||||
clearTimeout(id);
|
clearTimeout(id);
|
||||||
|
@ -87,6 +87,30 @@ test('an empty pool', function() {
|
|||||||
pool.checkIn(external);
|
pool.checkIn(external);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('when creating async new pool members', function() {
|
||||||
|
var count = 0;
|
||||||
|
var pool = new Pool(3, function() {
|
||||||
|
var item = {ref: {name: ++count}, checkedIn: false};
|
||||||
|
process.nextTick(function() {
|
||||||
|
pool.checkIn(item.ref)
|
||||||
|
})
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
test('one request recieves member', function() {
|
||||||
|
pool.checkOut(assert.calls(function(err, item) {
|
||||||
|
assert.equal(item.name, 1)
|
||||||
|
pool.checkOut(assert.calls(function(err, item) {
|
||||||
|
assert.equal(item.name, 2)
|
||||||
|
pool.checkOut(assert.calls(function(err, item) {
|
||||||
|
assert.equal(item.name, 3)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user