GH-51 Timeout support in client library
This commit is contained in:
parent
44a9d87863
commit
f7276475b9
|
@ -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);
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue