yuna
Version:
Extra zookeeper support for Illyria.
437 lines (376 loc) • 12.1 kB
JavaScript
/**
* XadillaX created at 2015-05-15 20:59:36 With ♥
*
* Copyright (c) 2015 Huaban.com, all rights
* reserved.
*/
require("sugar");
var uuid = require("node-uuid");
var async = require("async");
var util = require("util");
var LinkList = require("algorithmjs").ds.Linklist;
var Illyria = require("illyria");
var EventEmitter = require("events").EventEmitter;
var Scarlet = require("scarlet-task");
var emptyCallback = function(){};
var DEFAULT_OPTIONS = {
runTimeout: 10000,
connectTimeout: 5000,
retriesForGetting: 5,
gettingRetryInterval: 200
};
var DEFAULT_CONNECTION_STRING = "127.0.0.1:2181";
var DEFAULT_ROOT = "/illyria";
var DEFAULT_PREFIX = "/HB_";
/**
* IllyriaPool
*
* @param {String|Array} [connectionString="127.0.0.1:2181"] the connection string or array
* @param {String} [root="/illyria"] the service root
* @param {String} [prefix="/HB_"] the service prefix
* @param {Object} [options={}] the connect options (especially in zookeeper)
* @param {Number} [options.runTimeout=10000] the run timeout value
* @param {Number} [options.connectTimeout=5000] the connect timeout value
* @param {Number} [options.maxPool=10] the max pool
* @param {Number} [options.retriesForGetting=5] retry times for getting usable client
* @param {Number} [options.gettingRetryInterval=200] retry interval per getting usable client (in million second)
*
* @constructor
*/
var IllyriaPool = function(connectionString, root, prefix, options) {
EventEmitter.call(this);
if(arguments.length === 0) {
connectionString = DEFAULT_CONNECTION_STRING;
root = DEFAULT_ROOT;
prefix = DEFAULT_PREFIX;
options = DEFAULT_OPTIONS;
} else if(arguments.length === 1) {
if(util.isArray(connectionString) || typeof connectionString === "string") {
// if it's a real connection string
options = DEFAULT_OPTIONS;
} else {
// if it's an option
options = Object.merge(connectionString, DEFAULT_OPTIONS, true, false);
connectionString = DEFAULT_CONNECTION_STRING;
}
root = DEFAULT_ROOT;
prefix = DEFAULT_PREFIX;
} else if(arguments.length === 2) {
if(typeof root === "string") {
options = DEFAULT_OPTIONS;
} else {
options = Object.merge(root, DEFAULT_OPTIONS, true, false);
root = DEFAULT_ROOT;
}
prefix = DEFAULT_PREFIX;
} else if(arguments.length === 3) {
if(typeof prefix === "string") {
options = DEFAULT_OPTIONS;
} else {
options = Object.merge(prefix, DEFAULT_OPTIONS, true, false);
prefix = DEFAULT_PREFIX;
}
} else {
options = Object.merge(options, DEFAULT_OPTIONS, true, false);
}
this.options = {};
for(var key in options) {
if(!options.hasOwnProperty(key)) continue;
this.options[key] = options[key];
}
if(undefined === this.options.zookeeper) this.options.zookeeper = {};
this.options.zookeeper.connectString = connectionString;
this.options.zookeeper.root = root;
this.options.zookeeper.prefix = prefix;
this.options.reconnect = false;
this.maxPool = this.options.maxPool || 10;
this.pool = new LinkList();
// underscore -> camelize
this.pool.popBack = this.pool["pop_back"];
this.pool.popFront = this.pool["pop_front"];
this.pool.pushBack = this.pool["push_back"];
this.pool.pushFront = this.pool["push_front"];
// create task processor
var self = this;
var scarlet = new Scarlet(1);
var createTasker = function(taskObject) {
self.newConnection(function() {
scarlet.taskDone(taskObject);
});
};
// create clients
for(var i = 0; i < this.maxPool; i++) {
scarlet.push(null, createTasker);
}
this.destroyed = false;
this.on("error", emptyCallback);
};
util.inherits(IllyriaPool, EventEmitter);
/**
* available count
* @return {Number} the available connection count
*/
IllyriaPool.prototype.availableCount = function() {
var i = 0;
var list = this.pool;
var count = 0;
for(var node = list._head.next; node !== list._tail; node = node.next, i++) {
if(node.value instanceof Illyria.Client && node.value.status === "CONNECTED") {
count++;
}
}
return count;
};
/**
* removeClient
* @param {IllyriaClient} client the illyria client
*/
IllyriaPool.prototype.removeClient = function(client) {
// remove this client from pool
var idx = this.clientPosition(client);
// if no node found
if(-1 !== idx) {
// remove node
this.pool.removeAt(idx);
}
try {
client.removeAllListeners();
// close client
client.close();
} catch(e) {
//... ignore error
}
};
/**
* clientPosition
* @param {IllyriaClient} client the illyria client
* @return {Number} the index of this client in the pool
*/
IllyriaPool.prototype.clientPosition = function(client) {
var i = 0;
var list = this.pool;
for(var node = list._head.next; node !== list._tail; node = node.next, i++) {
if(node.value === client) return i;
}
return -1;
};
/**
* newConnection
* @param {Boolean} [force] create new client forcely
* @param {Function} [callback] the callback function
*/
IllyriaPool.prototype.newConnection = function(force, callback) {
if(typeof force === "function") {
callback = force, force = false;
}
if(undefined === callback) callback = emptyCallback;
/**
* if the Yuna has been destroyed.
*/
if(this.destroyed) {
return process.nextTick(function() {
callback(new Error("This Yuna has been destroyed."));
});
}
/**
* if the existing client count is larger then expected and the action is not
* force
*/
if(this.pool.length >= this.maxPool && !force) {
return process.nextTick(function() {
callback(new Error(this.maxPool + " connection limit exceeded."));
});
}
var client = Illyria.createClient(this.options);
this.pool.pushBack(client);
// add some extra properties
client.poolNode = this.pool.nodeAt(this.pool.length - 1);
client.taskCount = 0;
client.uuid = uuid.v1();
client.$send = client.send;
client.$cast = client.cast;
client.send = (function(module, method, params, callback) {
var self = this;
this.taskCount++;
this.$send(module, method, params, function() {
self.taskCount--;
callback.apply(null, arguments);
});
}).bind(client);
client.cast = (function(module, method, params, callback) {
var self = this;
this.taskCount++;
this.$cast(module, method, params, function() {
self.taskCount--;
callback.apply(null, arguments);
});
}).bind(client);
// connect and wait for timeout
var self = this;
var timeouted = false;
var timer = setTimeout(function() {
timeouted = true;
self.removeClient(client);
var err = new Error("Connect timeout after " + self.options.connectTimeout + "ms.");
callback(err);
self.emit("error", err);
}, this.options.connectTimeout);
client.connect(function(err) {
clearTimeout(timer);
if(timeouted) {
return self.removeClient(client);
}
if(err) {
self.removeClient(client);
var _err;
if(err.message.indexOf("NO_NODE") >= 0) {
_err = new Error("Error occurred while connecting: No available server found.");
} else {
_err = new Error("Error occurred while connecting: " + err.message);
}
self.emit("error", _err);
return callback(err);
}
callback(undefined, client);
self.emit("new", client);
var onClose = function() {
// remove this client and create a new one
self.removeClient(client);
self.newConnection();
self.emit("close");
};
client.on("close", onClose);
client.on("error", onClose);
});
};
/**
* getConnection
* @param {Function} callback the callback function
*/
IllyriaPool.prototype.getConnection = function(callback) {
/**
* if the Yuna has been destroyed.
*/
if(this.destroyed) {
return process.nextTick(function() {
callback(new Error("This Yuna has been destroyed."));
});
}
var self = this;
if(!this.pool.length) {
return this.newConnection(callback);
}
// traversal the list and get minimum tasks ones
var list = this.pool;
var clients = [];
var min = 0;
for(var node = list._head.next; node !== list._tail; node = node.next) {
var _client = node.value;
if(_client.connectStatus() !== "CONNECTED") {
continue;
}
if(!clients.length) {
clients.push(_client);
min = _client.taskCount;
} else if(min > _client.taskCount) {
clients = [ _client ];
min = _client.taskCount;
}
}
// no usable connection
if(!clients.length) {
if(this.pool.length < this.maxPool) {
return this.newConnection(callback);
}
// for retry
var retries = arguments[1];
if(!retries || retries < this.options.retriesForGetting) {
return setTimeout(function() {
self.getConnection(callback, (retries || 0) + 1);
}, this.options.gettingRetryInterval);
}
// no more retry
return process.nextTick(function() {
callback(new Error("No usable client node now."));
});
}
// get a random client in `clients`
var client = clients[Number.random(0, clients.length - 1)];
// return current client
process.nextTick(function() {
callback(undefined, client);
});
};
/**
* cast
* @param {String} module the module name
* @param {String} method the method name
* @param {*} params the params
* @param {Function} callback the callback function
*/
IllyriaPool.prototype.cast = function(module, method, params, callback_) {
this.send(module, method, params, callback_, true);
};
/**
* send
* @param {String} module the module name
* @param {String} method the method name
* @param {*} params the params
* @param {Function} callback the callback function
*/
IllyriaPool.prototype.send = function(module, method, params, callback_) {
var self = this;
var argv = arguments;
var isCast = false;
/**
* if the Yuna has been destroyed.
*/
if(this.destroyed) {
return process.nextTick(function() {
callback_(new Error("This Yuna has been destroyed."));
});
}
var _client;
async.waterfall([
/**
* step 1.
* select a usable client at first
*/
function(callback) {
self.getConnection(function(err, client) {
if(err) {
return callback(err);
}
callback(undefined, client);
});
},
/**
* step 2.
* send the message
*/
function(client, callback) {
_client = client;
// a hidden param - isCast...
// it's after callback
if(argv.length >= 5) {
isCast = !!argv[4];
}
var func = isCast ? client.cast.bind(client) : client.send.bind(client);
func(module, method, params, callback);
}
], function(err, data) {
callback_(err, data, _client);
});
};
/**
* destroy
*/
IllyriaPool.prototype.destroy = function() {
while(this.pool.length) {
var conn = this.pool.popBack();
conn.removeAllListeners();
conn.close();
}
this.destroyed = true;
};
module.exports = IllyriaPool;