GH-51 Timeout support in client library

This commit is contained in:
Mark Cavage 2012-01-19 18:02:10 -08:00
parent 44a9d87863
commit f7276475b9
5 changed files with 122 additions and 59 deletions

View File

@ -38,6 +38,7 @@ var opts = {
'password': String, 'password': String,
'persistent': Boolean, 'persistent': Boolean,
'scope': String, 'scope': String,
'timeout': Number,
'url': url 'url': url
}; };
@ -49,6 +50,7 @@ var shortOpts = {
'w': ['--password'], 'w': ['--password'],
'p': ['--persistent'], 'p': ['--persistent'],
's': ['--scope'], 's': ['--scope'],
't': ['--timeout'],
'u': ['--url'] 'u': ['--url']
}; };
@ -129,13 +131,19 @@ if (!parsed.persistent)
var client = ldap.createClient({ var client = ldap.createClient({
url: parsed.url, url: parsed.url,
log4js: log4js log4js: log4js,
timeout: parsed.timeout || false
}); });
client.on('error', function(err) { client.on('error', function(err) {
perror(err); perror(err);
}); });
client.on('timeout', function(req) {
process.stderr.write('Timeout reached\n');
process.exit(1);
});
client.bind(parsed.binddn, parsed.password, function(err, res) { client.bind(parsed.binddn, parsed.password, function(err, res) {
if (err) if (err)
perror(err); perror(err);

View File

@ -28,7 +28,8 @@ client is:
||url|| a valid LDAP url.|| ||url|| a valid LDAP url.||
||socketPath|| If you're running an LDAP server over a Unix Domain Socket, use this.|| ||socketPath|| If you're running an LDAP server over a Unix Domain Socket, use this.||
||log4js|| You can optionally pass in a log4js instance the client will use to acquire a logger. The client logs all messages at the `Trace` level.|| ||log4js|| You can optionally pass in a log4js instance the client will use to acquire a logger. The client logs all messages at the `Trace` level.||
||numConnections||The size of the connection pool. Default is 1.|| ||timeout||How long the client should let operations live for before timing out. Default is Infinity.||
||connectTimeout||How long the client should wait before timing out on TCP connections. Default is up to the OS.||
||reconnect||Whether or not to automatically reconnect (and rebind) on socket errors. Takes amount of time in millliseconds. Default is 1000. 0/false will disable altogether.|| ||reconnect||Whether or not to automatically reconnect (and rebind) on socket errors. Takes amount of time in millliseconds. Default is 1000. 0/false will disable altogether.||
## Connection management ## Connection management
@ -45,6 +46,14 @@ operations be allowed back through; in the meantime all callbacks will receive
a `DisconnectedError`. If you never called `bind`, the client will allow a `DisconnectedError`. If you never called `bind`, the client will allow
operations when the socket is connected. operations when the socket is connected.
Also, note that the client will emit a `timeout` event if an operation
times out, and you'll be passed in the request object that was offending. You
probably don't _need_ to listen on it, as the client will also return an error
in the callback of that request. However, it is useful if you want to have a
catch-all. An event of `connectTimout` will be emitted when the client fails to
get a socket in time; there are no arguments. Note that this event will be
emitted (potentially) in reconnect scenarios as well.
## Common patterns ## Common patterns
The last two parameters in every API are `controls` and `callback`. `controls` The last two parameters in every API are `controls` and `callback`. `controls`

View File

@ -124,13 +124,18 @@ function Client(options) {
this.secure = this.url.secure; this.secure = this.url.secure;
} }
this.log4js = options.log4js || logStub; this.connection = null;
this.connectTimeout = options.connectTimeout || false;
this.connectOptions = { this.connectOptions = {
port: self.url ? self.url.port : options.socketPath, port: self.url ? self.url.port : options.socketPath,
host: self.url ? self.url.hostname : undefined, host: self.url ? self.url.hostname : undefined,
socketPath: options.socketPath || undefined socketPath: options.socketPath || undefined
}; };
this.log4js = options.log4js || logStub;
this.reconnect = (typeof(options.reconnect) === 'number' ?
options.reconnect : 1000);
this.shutdown = false; this.shutdown = false;
this.timeout = options.timeout || false;
this.__defineGetter__('log', function() { this.__defineGetter__('log', function() {
if (!self._log) if (!self._log)
@ -139,11 +144,7 @@ function Client(options) {
return self._log; return self._log;
}); });
this.reconnect = (typeof(options.reconnect) === 'number' ? return this.connect(function() {});
options.reconnect : 1000);
this.connection = null;
this.connect();
} }
util.inherits(Client, EventEmitter); util.inherits(Client, EventEmitter);
module.exports = Client; module.exports = Client;
@ -157,24 +158,39 @@ module.exports = Client;
*/ */
Client.prototype.connect = function(callback) { Client.prototype.connect = function(callback) {
if (this.connection) if (this.connection)
return; return callback();
var self = this; var self = this;
var connection = this.connection = this._newConnection(); var timer = false;
if (this.connectTimeout) {
timer = setTimeout(function() {
if (self.connection)
self.connection.destroy();
var err = new ConnectionError('timeout');
self.emit('connectTimeout');
return callback(err);
}, this.connectTimeout);
}
this.connection = this._newConnection();
function reconnect() { function reconnect() {
self.connection = null; self.connection = null;
if (self.reconnect) if (self.reconnect)
setTimeout(function() { self.connect(); }, self.reconnect); setTimeout(function() { self.connect(function() {}); }, self.reconnect);
} }
this.connection.on('close', function(had_err) { self.connection.on('close', function(had_err) {
self.emit('close', had_err); self.emit('close', had_err);
reconnect(); reconnect();
}); });
this.connection.on('connect', function() { self.connection.on('connect', function() {
if (timer)
clearTimeout(timer);
if (self._bindDN && self._credentials) if (self._bindDN && self._credentials)
return self.bind(self._bindDN, self._credentials, function(err) { return self.bind(self._bindDN, self._credentials, function(err) {
if (err) { if (err) {
@ -183,9 +199,11 @@ Client.prototype.connect = function(callback) {
} }
self.emit('connect'); self.emit('connect');
return callback();
}); });
self.emit('connect'); self.emit('connect');
return callback();
}); });
}; };
@ -214,9 +232,9 @@ Client.prototype.bind = function(name, credentials, controls, callback, conn) {
if (typeof(callback) !== 'function') if (typeof(callback) !== 'function')
throw new TypeError('callback (function) required'); throw new TypeError('callback (function) required');
this.connect();
var self = this;
var self = this;
this.connect(function() {
var req = new BindRequest({ var req = new BindRequest({
name: name || '', name: name || '',
authentication: 'Simple', authentication: 'Simple',
@ -232,6 +250,7 @@ Client.prototype.bind = function(name, credentials, controls, callback, conn) {
return callback(err, res); return callback(err, res);
}, conn); }, conn);
});
}; };
@ -691,7 +710,7 @@ Client.prototype.unbind = function(callback) {
this._bindDN = null; this._bindDN = null;
this._credentials = null; this._credentials = null;
if (!this.connect) if (!this.connection)
return callback(); return callback();
var req = new UnbindRequest(); var req = new UnbindRequest();
@ -708,21 +727,22 @@ Client.prototype._send = function(message, expect, callback, connection) {
var conn = connection || this.connection; var conn = connection || this.connection;
var self = this; var self = this;
var timer;
function closeConn(err) { function closeConn(err) {
if (timer)
clearTimeout(timer);
var err = err || new ConnectionError('no connection'); var err = err || new ConnectionError('no connection');
// This is lame, but we want to send the original error back, whereas if (typeof(callback) === 'function') {
// this will trigger a connection event callback(err);
process.nextTick(function() { } else {
callback.emit('error', err);
}
if (conn) if (conn)
conn.destroy(); conn.destroy();
});
if (typeof(callback) === 'function')
return callback(err);
return callback.emit('error', err);
} }
if (!conn) if (!conn)
@ -731,7 +751,10 @@ Client.prototype._send = function(message, expect, callback, connection) {
// Now set up the callback in the messages table // Now set up the callback in the messages table
message.messageID = conn.ldap.nextMessageID; message.messageID = conn.ldap.nextMessageID;
if (expect !== 'abandon') { if (expect !== 'abandon') {
conn.ldap.messages[message.messageID] = function(res) { conn.ldap.messages[message.messageID] = function _sendCallback(res) {
if (timer)
clearTimeout(timer);
if (self.log.isDebugEnabled()) if (self.log.isDebugEnabled())
self.log.debug('%s: response received: %j', conn.ldap.id, res.json); self.log.debug('%s: response received: %j', conn.ldap.id, res.json);
@ -778,10 +801,19 @@ Client.prototype._send = function(message, expect, callback, connection) {
}; };
} }
// Finally send some data // If there's a user specified timeout, pick that up
if (this.log.isDebugEnabled()) if (this.timeout) {
this.log.debug('%s: sending request: %j', conn.ldap.id, message.json); timer = setTimeout(function() {
self.emit('timeout', message);
if (conn.ldap.messages[message.messageID])
return conn.ldap.messages[message.messageID](new LDAPResult({
status: 80, // LDAP_OTHER
errorMessage: 'request timeout (client interrupt)'
}));
}, this.timeout);
}
try {
// Note if this was an unbind, we just go ahead and end, since there // Note if this was an unbind, we just go ahead and end, since there
// will never be a response // will never be a response
var _writeCb = null; var _writeCb = null;
@ -794,11 +826,11 @@ Client.prototype._send = function(message, expect, callback, connection) {
conn.unbindMessageID = message.id; conn.unbindMessageID = message.id;
conn.end(); conn.end();
}; };
} else {
// noop
} }
try { // Finally send some data
if (this.log.isDebugEnabled())
this.log.debug('%s: sending request: %j', conn.ldap.id, message.json);
return conn.write(message.toBer(), _writeCb); return conn.write(message.toBer(), _writeCb);
} catch (e) { } catch (e) {
return closeConn(e); return closeConn(e);
@ -906,6 +938,7 @@ Client.prototype._newConnection = function() {
if (log.isTraceEnabled()) if (log.isTraceEnabled())
log.trace('%s timeout event=%s', c.ldap.id); log.trace('%s timeout event=%s', c.ldap.id);
self.emit('timeout');
c.end(); c.end();
}); });

View File

@ -75,6 +75,10 @@ test('setup', function(t) {
return next(); return next();
}); });
server.search('dc=timeout', function(req, res, next) {
// Haha client!
});
server.search(SUFFIX, function(req, res, next) { server.search(SUFFIX, function(req, res, next) {
if (req.dn.equals('cn=ref,' + SUFFIX)) { if (req.dn.equals('cn=ref,' + SUFFIX)) {
@ -575,6 +579,15 @@ test('unbind (GH-30)', function(t) {
}); });
test('search timeout (GH-51)', function(t) {
client.timeout = 250;
client.search('dc=timeout', 'objectclass=*', function(err, res) {
t.ok(err);
t.end();
});
});
test('shutdown', function(t) { test('shutdown', function(t) {
client.unbind(function(err) { client.unbind(function(err) {
server.on('close', function() { server.on('close', function() {