You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
735 lines
22 KiB
735 lines
22 KiB
/** |
|
* Copyright (c) 2013 Yahoo! Inc. All rights reserved. |
|
* |
|
* Copyrights licensed under the MIT License. See the accompanying LICENSE file |
|
* for terms. |
|
*/ |
|
|
|
var net = require('net'); |
|
var utils = require('util'); |
|
var events = require('events'); |
|
|
|
var jute = require('./jute'); |
|
var ConnectionStringParser = require('./ConnectionStringParser.js'); |
|
var WatcherManager = require('./WatcherManager.js'); |
|
var PacketQueue = require('./PacketQueue.js'); |
|
var Exception = require('./Exception.js'); |
|
|
|
/** |
|
* This class manages the connection between the client and the ensemble. |
|
* |
|
* @module node-zookeeper-client |
|
*/ |
|
|
|
// Constants. |
|
var STATES = { // Connection States. |
|
DISCONNECTED : 0, |
|
CONNECTING : 1, |
|
CONNECTED : 2, |
|
CONNECTED_READ_ONLY : 3, |
|
CLOSING : -1, |
|
CLOSED : -2, |
|
SESSION_EXPIRED : -3, |
|
AUTHENTICATION_FAILED : -4 |
|
}; |
|
|
|
|
|
/** |
|
* Construct a new ConnectionManager instance. |
|
* |
|
* @class ConnectionStringParser |
|
* @constructor |
|
* @param connectionString {String} ZooKeeper server ensemble string. |
|
* @param options {Object} Client options. |
|
* @param stateListener {Object} Listener for state changes. |
|
*/ |
|
function ConnectionManager(connectionString, options, stateListener) { |
|
events.EventEmitter.call(this); |
|
|
|
this.watcherManager = new WatcherManager(); |
|
this.connectionStringParser = new ConnectionStringParser(connectionString); |
|
|
|
this.servers = this.connectionStringParser.getServers(); |
|
this.chrootPath = this.connectionStringParser.getChrootPath(); |
|
this.nextServerIndex = 0; |
|
this.serverAttempts = 0; |
|
|
|
this.state = STATES.DISCONNECTED; |
|
|
|
this.options = options; |
|
this.spinDelay = options.spinDelay; |
|
|
|
this.updateTimeout(options.sessionTimeout); |
|
this.connectTimeoutHandler = null; |
|
|
|
this.xid = 0; |
|
|
|
this.sessionId = new Buffer(8); |
|
if (Buffer.isBuffer(options.sessionId)) { |
|
options.sessionId.copy(this.sessionId); |
|
} else { |
|
this.sessionId.fill(0); |
|
} |
|
|
|
this.sessionPassword = new Buffer(16); |
|
if (Buffer.isBuffer(options.sessionPassword)) { |
|
options.sessionPassword.copy(this.sessionPassword); |
|
} else { |
|
this.sessionPassword.fill(0); |
|
} |
|
|
|
// scheme:auth pairs |
|
this.credentials = []; |
|
|
|
// Last seen zxid. |
|
this.zxid = new Buffer(8); |
|
this.zxid.fill(0); |
|
|
|
|
|
this.pendingBuffer = null; |
|
|
|
this.packetQueue = new PacketQueue(); |
|
this.packetQueue.on('readable', this.onPacketQueueReadable.bind(this)); |
|
this.pendingQueue = []; |
|
|
|
this.on('state', stateListener); |
|
} |
|
|
|
utils.inherits(ConnectionManager, events.EventEmitter); |
|
|
|
/** |
|
* Update the session timeout and related timeout variables. |
|
* |
|
* @method updateTimeout |
|
* @private |
|
* @param sessionTimeout {Number} Milliseconds of the timeout value. |
|
*/ |
|
ConnectionManager.prototype.updateTimeout = function (sessionTimeout) { |
|
this.sessionTimeout = sessionTimeout; |
|
|
|
// Designed to have time to try all the servers. |
|
this.connectTimeout = Math.floor(sessionTimeout / this.servers.length); |
|
|
|
// We at least send out one ping one third of the session timeout, so |
|
// the read timeout is two third of the session timeout. |
|
this.pingTimeout = Math.floor(this.sessionTimeout / 3); |
|
// this.readTimeout = Math.floor(sessionTimeout * 2 / 3); |
|
}; |
|
|
|
/** |
|
* Find the next available server to connect. If all server has been tried, |
|
* it will wait for a random time between 0 to spin delay before call back |
|
* with the next server. |
|
* |
|
* callback prototype: |
|
* callback(server); |
|
* |
|
* @method findNextServer |
|
* @param callback {Function} callback function. |
|
* |
|
*/ |
|
ConnectionManager.prototype.findNextServer = function (callback) { |
|
var self = this; |
|
|
|
self.nextServerIndex %= self.servers.length; |
|
|
|
if (self.serverAttempts === self.servers.length) { |
|
setTimeout(function () { |
|
callback(self.servers[self.nextServerIndex]); |
|
self.nextServerIndex += 1; |
|
|
|
// reset attempts since we already waited for enough time. |
|
self.serverAttempts = 0; |
|
}, Math.random() * self.spinDelay); |
|
} else { |
|
self.serverAttempts += 1; |
|
|
|
process.nextTick(function () { |
|
callback(self.servers[self.nextServerIndex]); |
|
self.nextServerIndex += 1; |
|
}); |
|
} |
|
}; |
|
|
|
/** |
|
* Change the current state to the given state if the given state is different |
|
* from current state. Emit the state change event with the changed state. |
|
* |
|
* @method setState |
|
* @param state {Number} The state to be set. |
|
*/ |
|
ConnectionManager.prototype.setState = function (state) { |
|
if (typeof state !== 'number') { |
|
throw new Error('state must be a valid number.'); |
|
} |
|
|
|
if (this.state !== state) { |
|
this.state = state; |
|
this.emit('state', this.state); |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.registerDataWatcher = function (path, watcher) { |
|
this.watcherManager.registerDataWatcher(path, watcher); |
|
}; |
|
|
|
ConnectionManager.prototype.registerChildWatcher = function (path, watcher) { |
|
this.watcherManager.registerChildWatcher(path, watcher); |
|
}; |
|
|
|
ConnectionManager.prototype.registerExistenceWatcher = function (path, watcher) { |
|
this.watcherManager.registerExistenceWatcher(path, watcher); |
|
}; |
|
|
|
ConnectionManager.prototype.cleanupPendingQueue = function (errorCode) { |
|
var pendingPacket = this.pendingQueue.shift(); |
|
|
|
while (pendingPacket) { |
|
if (pendingPacket.callback) { |
|
pendingPacket.callback(Exception.create(errorCode)); |
|
} |
|
|
|
pendingPacket = this.pendingQueue.shift(); |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.getSessionId = function () { |
|
var result = new Buffer(8); |
|
|
|
this.sessionId.copy(result); |
|
return result; |
|
}; |
|
|
|
ConnectionManager.prototype.getSessionPassword = function () { |
|
var result = new Buffer(16); |
|
|
|
this.sessionPassword.copy(result); |
|
return result; |
|
}; |
|
|
|
ConnectionManager.prototype.getSessionTimeout = function () { |
|
return this.sessionTimeout; |
|
}; |
|
|
|
ConnectionManager.prototype.connect = function () { |
|
var self = this; |
|
|
|
self.setState(STATES.CONNECTING); |
|
|
|
self.findNextServer(function (server) { |
|
self.socket = net.connect(server); |
|
|
|
self.connectTimeoutHandler = setTimeout( |
|
self.onSocketConnectTimeout.bind(self), |
|
self.connectTimeout |
|
); |
|
|
|
// Disable the Nagle algorithm. |
|
self.socket.setNoDelay(); |
|
|
|
self.socket.on('connect', self.onSocketConnected.bind(self)); |
|
self.socket.on('data', self.onSocketData.bind(self)); |
|
self.socket.on('drain', self.onSocketDrain.bind(self)); |
|
self.socket.on('close', self.onSocketClosed.bind(self)); |
|
self.socket.on('error', self.onSocketError.bind(self)); |
|
}); |
|
}; |
|
|
|
ConnectionManager.prototype.close = function () { |
|
var self = this, |
|
header = new jute.protocol.RequestHeader(), |
|
request; |
|
|
|
self.setState(STATES.CLOSING); |
|
|
|
header.type = jute.OP_CODES.CLOSE_SESSION; |
|
request = new jute.Request(header, null); |
|
|
|
self.queue(request); |
|
}; |
|
|
|
ConnectionManager.prototype.onSocketClosed = function (hasError) { |
|
var retry = false, |
|
errorCode, |
|
pendingPacket; |
|
|
|
switch (this.state) { |
|
case STATES.CLOSING: |
|
errorCode = Exception.CONNECTION_LOSS; |
|
retry = false; |
|
break; |
|
case STATES.SESSION_EXPIRED: |
|
errorCode = Exception.SESSION_EXPIRED; |
|
retry = false; |
|
break; |
|
case STATES.AUTHENTICATION_FAILED: |
|
errorCode = Exception.AUTH_FAILED; |
|
retry = false; |
|
break; |
|
default: |
|
errorCode = Exception.CONNECTION_LOSS; |
|
retry = true; |
|
} |
|
|
|
this.cleanupPendingQueue(errorCode); |
|
this.setState(STATES.DISCONNECTED); |
|
|
|
if (retry) { |
|
this.connect(); |
|
} else { |
|
this.setState(STATES.CLOSED); |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.onSocketError = function (error) { |
|
if (this.connectTimeoutHandler) { |
|
clearTimeout(this.connectTimeoutHandler); |
|
} |
|
|
|
// After socket error, the socket closed event will be triggered, |
|
// we will retry connect in that listener function. |
|
}; |
|
|
|
ConnectionManager.prototype.onSocketConnectTimeout = function () { |
|
// Destroy the current socket so the socket closed event |
|
// will be trigger. |
|
this.socket.destroy(); |
|
}; |
|
|
|
ConnectionManager.prototype.onSocketConnected = function () { |
|
var connectRequest, |
|
authRequest, |
|
setWatchesRequest, |
|
header, |
|
payload; |
|
|
|
if (this.connectTimeoutHandler) { |
|
clearTimeout(this.connectTimeoutHandler); |
|
} |
|
|
|
connectRequest = new jute.Request(null, new jute.protocol.ConnectRequest( |
|
jute.PROTOCOL_VERSION, |
|
this.zxid, |
|
this.sessionTimeout, |
|
this.sessionId, |
|
this.sessionPassword |
|
)); |
|
|
|
// XXX No read only support yet. |
|
this.socket.write(connectRequest.toBuffer()); |
|
|
|
// Set auth info |
|
if (this.credentials.length > 0) { |
|
this.credentials.forEach(function (credential) { |
|
header = new jute.protocol.RequestHeader(); |
|
payload = new jute.protocol.AuthPacket(); |
|
|
|
header.xid = jute.XID_AUTHENTICATION; |
|
header.type = jute.OP_CODES.AUTH; |
|
|
|
payload.type = 0; |
|
payload.scheme = credential.scheme; |
|
payload.auth = credential.auth; |
|
|
|
authRequest = new jute.Request(header, payload); |
|
this.queue(authRequest); |
|
|
|
}, this); |
|
} |
|
|
|
// Reset the watchers if we have any. |
|
if (!this.watcherManager.isEmpty()) { |
|
header = new jute.protocol.RequestHeader(); |
|
payload = new jute.protocol.SetWatches(); |
|
|
|
header.type = jute.OP_CODES.SET_WATCHES; |
|
header.xid = jute.XID_SET_WATCHES; |
|
|
|
payload.setChrootPath(this.chrootPath); |
|
payload.relativeZxid = this.zxid; |
|
payload.dataWatches = this.watcherManager.getDataWatcherPaths(); |
|
payload.existWatches = this.watcherManager.getExistenceWatcherPaths(); |
|
payload.childWatches = this.watcherManager.getChildWatcherPaths(); |
|
|
|
setWatchesRequest = new jute.Request(header, payload); |
|
this.queue(setWatchesRequest); |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.onSocketTimeout = function () { |
|
var header, |
|
request; |
|
|
|
if (this.socket && |
|
(this.state === STATES.CONNECTED || |
|
this.state === STATES.CONNECTED_READ_ONLY)) { |
|
header = new jute.protocol.RequestHeader( |
|
jute.XID_PING, |
|
jute.OP_CODES.PING |
|
); |
|
|
|
request = new jute.Request(header, null); |
|
this.queue(request); |
|
|
|
// Re-register the timeout handler since it only fired once. |
|
this.socket.setTimeout( |
|
this.pingTimeout, |
|
this.onSocketTimeout.bind(this) |
|
); |
|
} |
|
}; |
|
|
|
/* eslint-disable complexity,max-depth */ |
|
ConnectionManager.prototype.onSocketData = function (buffer) { |
|
var self = this, |
|
offset = 0, |
|
size = 0, |
|
connectResponse, |
|
pendingPacket, |
|
responseHeader, |
|
responsePayload, |
|
response, |
|
event; |
|
|
|
// Combine the pending buffer with the new buffer. |
|
if (self.pendingBuffer) { |
|
buffer = Buffer.concat( |
|
[self.pendingBuffer, buffer], |
|
self.pendingBuffer.length + buffer.length |
|
); |
|
} |
|
|
|
// We need at least 4 bytes |
|
if (buffer.length < 4) { |
|
self.pendingBuffer = buffer; |
|
return; |
|
} |
|
|
|
size = buffer.readInt32BE(offset); |
|
offset += 4; |
|
|
|
if (buffer.length < size + 4) { |
|
// More data are coming. |
|
self.pendingBuffer = buffer; |
|
return; |
|
} |
|
|
|
if (buffer.length === size + 4) { |
|
// The size is perfect. |
|
self.pendingBuffer = null; |
|
} else { |
|
// We have extra bytes, splice them out as pending buffer. |
|
self.pendingBuffer = buffer.slice(size + 4); |
|
buffer = buffer.slice(0, size + 4); |
|
} |
|
|
|
if (self.state === STATES.CONNECTING) { |
|
// Handle connect response. |
|
connectResponse = new jute.protocol.ConnectResponse(); |
|
offset += connectResponse.deserialize(buffer, offset); |
|
|
|
|
|
if (connectResponse.timeOut <= 0) { |
|
self.setState(STATES.SESSION_EXPIRED); |
|
|
|
} else { |
|
// Reset the server connection attempts since we connected now. |
|
self.serverAttempts = 0; |
|
|
|
self.sessionId = connectResponse.sessionId; |
|
self.sessionPassword = connectResponse.passwd; |
|
self.updateTimeout(connectResponse.timeOut); |
|
|
|
self.setState(STATES.CONNECTED); |
|
|
|
// Check if we have anything to send out just in case. |
|
self.onPacketQueueReadable(); |
|
|
|
self.socket.setTimeout( |
|
self.pingTimeout, |
|
self.onSocketTimeout.bind(self) |
|
); |
|
|
|
} |
|
} else { |
|
// Handle all other repsonses. |
|
responseHeader = new jute.protocol.ReplyHeader(); |
|
offset += responseHeader.deserialize(buffer, offset); |
|
|
|
// TODO BETTTER LOGGING |
|
switch (responseHeader.xid) { |
|
case jute.XID_PING: |
|
break; |
|
case jute.XID_AUTHENTICATION: |
|
if (responseHeader.err === Exception.AUTH_FAILED) { |
|
self.setState(STATES.AUTHENTICATION_FAILED); |
|
} |
|
break; |
|
case jute.XID_NOTIFICATION: |
|
event = new jute.protocol.WatcherEvent(); |
|
|
|
if (self.chrootPath) { |
|
event.setChrootPath(self.chrootPath); |
|
} |
|
|
|
offset += event.deserialize(buffer, offset); |
|
self.watcherManager.emit(event); |
|
break; |
|
default: |
|
pendingPacket = self.pendingQueue.shift(); |
|
|
|
if (!pendingPacket) { |
|
// TODO, better error handling and logging need to be done. |
|
// Need to clean up and do a reconnect. |
|
// throw new Error( |
|
// 'Nothing in pending queue but got data from server.' |
|
// ); |
|
self.socket.destroy(); // this will trigger reconnect |
|
return; |
|
} |
|
|
|
if (pendingPacket.request.header.xid !== responseHeader.xid) { |
|
// TODO, better error handling/logging need to bee done here. |
|
// Need to clean up and do a reconnect. |
|
// throw new Error( |
|
// 'Xid out of order. Got xid: ' + |
|
// responseHeader.xid + ' with error code: ' + |
|
// responseHeader.err + ', expected xid: ' + |
|
// pendingPacket.request.header.xid + '.' |
|
// ); |
|
self.socket.destroy(); // this will trigger reconnect |
|
return; |
|
} |
|
|
|
if (responseHeader.zxid) { |
|
// TODO, In Java implementation, the condition is to |
|
// check whether the long zxid is greater than 0, here |
|
// use buffer so we simplify. |
|
// Need to figure out side effect. |
|
self.zxid = responseHeader.zxid; |
|
} |
|
|
|
if (responseHeader.err === 0) { |
|
switch (pendingPacket.request.header.type) { |
|
case jute.OP_CODES.CREATE: |
|
responsePayload = new jute.protocol.CreateResponse(); |
|
break; |
|
case jute.OP_CODES.DELETE: |
|
responsePayload = null; |
|
break; |
|
case jute.OP_CODES.GET_CHILDREN2: |
|
responsePayload = new jute.protocol.GetChildren2Response(); |
|
break; |
|
case jute.OP_CODES.EXISTS: |
|
responsePayload = new jute.protocol.ExistsResponse(); |
|
break; |
|
case jute.OP_CODES.SET_DATA: |
|
responsePayload = new jute.protocol.SetDataResponse(); |
|
break; |
|
case jute.OP_CODES.GET_DATA: |
|
responsePayload = new jute.protocol.GetDataResponse(); |
|
break; |
|
case jute.OP_CODES.SET_ACL: |
|
responsePayload = new jute.protocol.SetACLResponse(); |
|
break; |
|
case jute.OP_CODES.GET_ACL: |
|
responsePayload = new jute.protocol.GetACLResponse(); |
|
break; |
|
case jute.OP_CODES.SET_WATCHES: |
|
responsePayload = null; |
|
break; |
|
case jute.OP_CODES.CLOSE_SESSION: |
|
responsePayload = null; |
|
break; |
|
case jute.OP_CODES.MULTI: |
|
responsePayload = new jute.TransactionResponse(); |
|
break; |
|
default: |
|
// throw new Error('Unknown request OP_CODE: ' + |
|
// pendingPacket.request.header.type); |
|
self.socket.destroy(); // this will trigger reconnect |
|
return; |
|
} |
|
|
|
if (responsePayload) { |
|
if (self.chrootPath) { |
|
responsePayload.setChrootPath(self.chrootPath); |
|
} |
|
|
|
offset += responsePayload.deserialize(buffer, offset); |
|
} |
|
|
|
if (pendingPacket.callback) { |
|
pendingPacket.callback( |
|
null, |
|
new jute.Response(responseHeader, responsePayload) |
|
); |
|
} |
|
} else if (pendingPacket.callback) { |
|
pendingPacket.callback( |
|
Exception.create(responseHeader.err), |
|
new jute.Response(responseHeader, null) |
|
); |
|
} |
|
} |
|
} |
|
|
|
// We have more data to process, need to recursively process it. |
|
if (self.pendingBuffer) { |
|
self.onSocketData(new Buffer(0)); |
|
} |
|
}; |
|
|
|
/* eslint-enable complexity,max-depth */ |
|
|
|
ConnectionManager.prototype.onSocketDrain = function () { |
|
// Trigger write on socket. |
|
this.onPacketQueueReadable(); |
|
}; |
|
|
|
ConnectionManager.prototype.onPacketQueueReadable = function () { |
|
var packet, |
|
header; |
|
|
|
switch (this.state) { |
|
case STATES.CONNECTED: |
|
case STATES.CONNECTED_READ_ONLY: |
|
case STATES.CLOSING: |
|
// Continue |
|
break; |
|
case STATES.DISCONNECTED: |
|
case STATES.CONNECTING: |
|
case STATES.CLOSED: |
|
case STATES.SESSION_EXPIRED: |
|
case STATES.AUTHENTICATION_FAILED: |
|
// Skip since we can not send traffic out |
|
return; |
|
default: |
|
throw new Error('Unknown state: ' + this.state); |
|
} |
|
|
|
while ((packet = this.packetQueue.shift()) !== undefined) { |
|
header = packet.request.header; |
|
if (header !== null && |
|
header.type !== jute.OP_CODES.PING && |
|
header.type !== jute.OP_CODES.AUTH) { |
|
|
|
header.xid = this.xid; |
|
this.xid += 1; |
|
|
|
// Only put requests that are not connect, ping and auth into |
|
// the pending queue. |
|
this.pendingQueue.push(packet); |
|
} |
|
|
|
if (!this.socket.write(packet.request.toBuffer())) { |
|
// Back pressure is handled here, when the socket emit |
|
// drain event, this method will be invoked again. |
|
break; |
|
} |
|
|
|
if (header.type === jute.OP_CODES.CLOSE_SESSION) { |
|
// The close session should be the final packet sent to the |
|
// server. |
|
break; |
|
} |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.addAuthInfo = function (scheme, auth) { |
|
if (!scheme || typeof scheme !== 'string') { |
|
throw new Error('scheme must be a non-empty string.'); |
|
} |
|
|
|
if (!Buffer.isBuffer(auth)) { |
|
throw new Error('auth must be a valid instance of Buffer'); |
|
} |
|
|
|
var header, |
|
payload, |
|
request; |
|
|
|
this.credentials.push({ |
|
scheme : scheme, |
|
auth : auth |
|
}); |
|
|
|
switch (this.state) { |
|
case STATES.CONNECTED: |
|
case STATES.CONNECTED_READ_ONLY: |
|
// Only queue the auth request when connected. |
|
header = new jute.protocol.RequestHeader(); |
|
payload = new jute.protocol.AuthPacket(); |
|
|
|
header.xid = jute.XID_AUTHENTICATION; |
|
header.type = jute.OP_CODES.AUTH; |
|
|
|
payload.type = 0; |
|
payload.scheme = scheme; |
|
payload.auth = auth; |
|
|
|
this.queue(new jute.Request(header, payload)); |
|
break; |
|
case STATES.DISCONNECTED: |
|
case STATES.CONNECTING: |
|
case STATES.CLOSING: |
|
case STATES.CLOSED: |
|
case STATES.SESSION_EXPIRED: |
|
case STATES.AUTHENTICATION_FAILED: |
|
// Skip when we are not in a live state. |
|
return; |
|
default: |
|
throw new Error('Unknown state: ' + this.state); |
|
} |
|
}; |
|
|
|
ConnectionManager.prototype.queue = function (request, callback) { |
|
if (typeof request !== 'object') { |
|
throw new Error('request must be a valid instance of jute.Request.'); |
|
} |
|
|
|
if (this.chrootPath && request.payload) { |
|
request.payload.setChrootPath(this.chrootPath); |
|
} |
|
|
|
|
|
callback = callback || function () {}; |
|
|
|
switch (this.state) { |
|
case STATES.DISCONNECTED: |
|
case STATES.CONNECTING: |
|
case STATES.CONNECTED: |
|
case STATES.CONNECTED_READ_ONLY: |
|
// queue the packet |
|
this.packetQueue.push({ |
|
request : request, |
|
callback : callback |
|
}); |
|
break; |
|
case STATES.CLOSING: |
|
if (request.header && |
|
request.header.type === jute.OP_CODES.CLOSE_SESSION) { |
|
this.packetQueue.push({ |
|
request : request, |
|
callback : callback |
|
}); |
|
} else { |
|
callback(Exception.create(Exception.CONNECTION_LOSS)); |
|
} |
|
break; |
|
case STATES.CLOSED: |
|
callback(Exception.create(Exception.CONNECTION_LOSS)); |
|
return; |
|
case STATES.SESSION_EXPIRED: |
|
callback(Exception.create(Exception.SESSION_EXPIRED)); |
|
return; |
|
case STATES.AUTHENTICATION_FAILED: |
|
callback(Exception.create(Exception.AUTH_FAILED)); |
|
return; |
|
default: |
|
throw new Error('Unknown state: ' + this.state); |
|
} |
|
}; |
|
|
|
module.exports = ConnectionManager; |
|
module.exports.STATES = STATES;
|
|
|