Improve client request state tracking

- Handle abandoned requests properly
- Re-enable client abandon test
- Report client idleness correctly (even after abandons)

Fix mcavage/node-ldapjs#211
This commit is contained in:
Patrick Mooney 2014-07-09 13:40:17 -05:00
parent 6607d83b86
commit 2939fa1fad
3 changed files with 168 additions and 53 deletions

View File

@ -141,7 +141,7 @@ RequestQueue.prototype.flush = function flush(cb) {
*/
RequestQueue.prototype.purge = function purge() {
this.flush(function (msg, expect, emitter, cb) {
cb(new Error('RequestQueue timeout'));
cb(new errors.TimeoutError('request queue timeout'));
});
};
@ -160,6 +160,118 @@ RequestQueue.prototype.thaw = function thaw() {
};
/**
* Track message callback by messageID.
*/
function MessageTracker(opts) {
assert.object(opts);
assert.string(opts.id);
assert.object(opts.parser);
this.id = opts.id;
this._msgid = 0;
this._messages = {};
this._abandoned = {};
this.parser = opts.parser;
var self = this;
this.__defineGetter__('pending', function () {
return Object.keys(self._messages);
});
}
/**
* Record a messageID and callback.
*/
MessageTracker.prototype.track = function track(message, callback) {
var msgid = this._nextID();
message.messageID = msgid;
this._messages[msgid] = callback;
return msgid;
};
/**
* Fetch callback based on messageID.
*/
MessageTracker.prototype.fetch = function fetch(msgid) {
var msg = this._messages[msgid];
if (msg) {
this._purgeAbandoned(msgid);
return msg;
}
// It's possible that the server has not received the abandon request yet.
// While waiting for evidence that the abandon has been received, incoming
// messages that match the abandoned msgid will be handled as normal.
msg = this._abandoned[msgid];
if (msg) {
return msg.cb;
}
return null;
};
/**
* Cease tracking for a given messageID.
*/
MessageTracker.prototype.remove = function remove(msgid) {
if (this._messages[msgid]) {
delete this._messages[msgid];
} else if (this._abandoned[msgid]) {
delete this._abandoned[msgid];
}
};
/**
* Mark a messageID as abandoned.
*/
MessageTracker.prototype.abandon = function abandonMsg(msgid) {
if (this._messages[msgid]) {
// Keep track of "when" the message was abandoned
this._abandoned[msgid] = {
age: this._msgid,
cb: this._messages[msgid]
};
delete this._messages[msgid];
}
};
/**
* Purge old items from abandoned list.
*/
MessageTracker.prototype._purgeAbandoned = function _purgeAbandoned(msgid) {
var self = this;
// Is (comp >= ref) according to sliding window
function geWindow(ref, comp) {
var max = ref + (MAX_MSGID/2);
var min = ref;
if (max >= MAX_MSGID) {
// Handle roll-over
max = max - MAX_MSGID - 1;
return ((comp <= max) || (comp >= min));
} else {
return ((comp <= max) && (comp >= min));
}
}
Object.keys(this._abandoned).forEach(function (id) {
// Abandoned messageIDs can be forgotten if a received messageID is "newer"
if (geWindow(self._abandoned[id].age, msgid)) {
self._abandoned[id].cb(new errors.AbandonedError(
'client request abandoned'));
delete self._abandoned[id];
}
});
};
/**
* Allocate the next messageID according to a sliding window.
*/
MessageTracker.prototype._nextID = function _nextID() {
if (++this._msgid >= MAX_MSGID)
this._msgid = 1;
return this._msgid;
};
///--- API
/**
@ -806,20 +918,10 @@ Client.prototype._connect = function _connect() {
// Initialize socket events and LDAP parser.
function initSocket() {
socket.ldap = {
socket.ldap = new MessageTracker({
id: self.url ? self.url.href : self.socketPath,
messageID: 0,
messages: {},
getNextMessageID: function getNextMessageID() {
if (++socket.ldap.messageID >= MAX_MSGID)
socket.ldap.messageID = 1;
return socket.ldap.messageID;
},
parser: new Parser({
log: log
})
};
parser: new Parser({log: log})
});
// This won't be set on TLS. So. Very. Annoying.
if (typeof (socket.setKeepAlive) !== 'function') {
@ -839,7 +941,7 @@ Client.prototype._connect = function _connect() {
// The "router"
socket.ldap.parser.on('message', function onMessage(message) {
message.connection = socket;
var callback = socket.ldap.messages[message.messageID];
var callback = socket.ldap.fetch(message.messageID);
if (!callback) {
log.error({message: message.json}, 'unsolicited message');
@ -1020,29 +1122,20 @@ Client.prototype._onClose = function _onClose(had_err) {
this.emit('close', had_err);
// On close we have to walk the outstanding messages and go invoke their
// callback with an error.
Object.keys(socket.ldap.messages).forEach(function (msgid) {
var err;
socket.ldap.pending.forEach(function (msgid) {
var cb = socket.ldap.fetch(msgid);
socket.ldap.remove(msgid);
if (socket.unbindMessageID !== parseInt(msgid, 10)) {
err = new ConnectionError(socket.ldap.id + ' closed');
return cb(new ConnectionError(socket.ldap.id + ' closed'));
} else {
// Unbinds will be communicated as a success since we're closed
err = new UnbindResponse({
messageID: msgid
});
err.status = 'unbind';
var unbind = new UnbindResponse({messageID: msgid});
unbind.status = 'unbind';
return cb(unbind);
}
if (typeof (socket.ldap.messages[msgid]) === 'function') {
var callback = socket.ldap.messages[msgid];
delete socket.ldap.messages[msgid];
return callback(err);
} else if (socket.ldap.messages[msgid]) {
if (err instanceof Error)
socket.ldap.messages[msgid].emit('error', err);
delete socket.ldap.messages[msgid];
}
return false;
});
delete socket.ldap.parser;
delete socket.ldap;
@ -1070,10 +1163,9 @@ Client.prototype._updateIdle = function _updateIdle(override) {
// Client must be connected but not waiting on any request data
var self = this;
function isIdle(disable) {
// FIXME: this breaks with abandons
return ((disable !== true) &&
(self.socket && self.connected) &&
(Object.keys(self.socket.ldap.messages).length === 0));
(self.socket.ldap.pending.length === 0));
}
if (isIdle(override)) {
if (!this._idleTimer) {
@ -1224,7 +1316,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
// page search continued, just return for now
return undefined;
} else {
delete conn.ldap.messages[message.messageID];
conn.ldap.remove(message.messageID);
// Potentially mark client as idle
self._updateIdle();
@ -1243,16 +1335,19 @@ Client.prototype._sendSocket = function _sendSocket(message,
function onRequestTimeout() {
self.emit('timeout', message);
if (conn.ldap.messages[message.messageID]) {
conn.ldap.messages[message.messageID](new LDAPResult({
status: 80, // LDAP_OTHER
errorMessage: 'request timeout (client interrupt)'
}));
var cb = conn.ldap.fetch(message.messageID);
if (cb) {
//FIXME: the timed-out request should be abandoned
cb(new errors.TimeoutError('request timeout (client interrupt)'));
}
} // end function onRequestTimeout()
function writeCallback() {
if (expect === 'abandon') {
// Mark the messageID specified as abandoned
conn.ldap.abandon(message.abandonID);
// No need to track the abandon request itself
conn.ldap.remove(message.id);
return callback(null);
} else if (expect === 'unbind') {
conn.unbindMessageID = message.id;
@ -1267,8 +1362,7 @@ Client.prototype._sendSocket = function _sendSocket(message,
} // end writeCallback()
// Start actually doing something...
message.messageID = conn.ldap.getNextMessageID();
conn.ldap.messages[message.messageID] = messageCallback;
conn.ldap.track(message, messageCallback);
// Mark client as active
this._updateIdle(true);

View File

@ -100,14 +100,37 @@ module.exports.getMessage = function (code) {
};
///--- Custom application errors
function ConnectionError(message) {
LDAPError.call(this,
'ConnectionError',
0x80, // LDAP_OTHER,
CODES.LDAP_OTHER,
message,
null,
ConnectionError);
}
util.inherits(ConnectionError, LDAPError);
module.exports.ConnectionError = ConnectionError;
function AbandonedError(message) {
LDAPError.call(this,
'AbandonedError',
CODES.LDAP_OTHER,
message,
null,
AbandonedError);
}
util.inherits(AbandonedError, LDAPError);
module.exports.AbandonedError = AbandonedError;
function TimeoutError(message) {
LDAPError.call(this,
'TimeoutError',
CODES.LDAP_OTHER,
message,
null,
TimeoutError);
}
util.inherits(TimeoutError, LDAPError);
module.exports.TimeoutError = TimeoutError;

View File

@ -619,7 +619,6 @@ test('GH-24 attribute selection of *', function (t) {
test('idle timeout', function (t) {
// FIXME: this must be run before abandon
client.idleTimeout = 250;
function premature() {
t.ifError(true);
@ -856,14 +855,13 @@ test('no auto-reconnect on unbind', function (t) {
});
// Abandon handling is improper on the server and not robust in the client
// FIXME: Disable test until one/both are fixed.
//test('abandon (GH-27)', function (t) {
// client.abandon(401876543, function (err) {
// t.ifError(err);
// t.end();
// });
//});
test('abandon (GH-27)', function (t) {
// FIXME: test abandoning a real request
client.abandon(401876543, function (err) {
t.ifError(err);
t.end();
});
});
test('search timeout (GH-51)', function (t) {