UNPKG

fast-redis-cluster2

Version:

Fast trully async redis cluster client library with HA support, can handle reshard, failovers etc...

500 lines (456 loc) 12.8 kB
var util = require('util'); var EventEmitter = require('events').EventEmitter; var redisCommands = require(__dirname+'/lib/commands.js'); var hostname = require('os').hostname(); var FastRedis = null; try { FastRedis = require('redis-fast-driver'); } catch(e) {} var RegularRedis = require('redis'); function RedisCluster(firstLink, options, cb) { var self = this; this.initialCallback = cb; this.options = options; this.queue = []; this.topology = { slots: {}, nodes: {} }; this.cmdId = 0; this.connected = false; this.renewTopologyInProgress = false; this.cacheLinks = {}; this.firstLink = firstLink; var first = this.getRedisClient(firstLink.host, firstLink.port, options.auth, options); first.on('ready', function(){ self.waitForTopology(); }); } util.inherits(RedisCluster, EventEmitter); RedisCluster.prototype.getRedisClient = function getRedisClient(host, port, auth, options) { var self = this; var key = host+':'+port; if(key in this.cacheLinks) { return this.cacheLinks[key]; } var link; if(!FastRedis || options.useFallbackDriver) { link = RegularRedis.createClient(port, host, options); link.rawCall = function mockRawCall(args, cb) { var cpArgs = args.slice(0); var cmd = cpArgs.shift(); link.send_command(cmd, cpArgs, cb); }; } else { link = new FastRedis({ host: host, port: port, auth: auth }); } link._queue = {}; this.cacheLinks[key] = link; link.on('error', function(err){ link.name = link.name || link.host+':'+link.port; console.log('Got error from link `%s`, err:', link.name, err); self.connected = false; var queue = link._queue; link._queue = {}; var id = link.id; link.end(); delete self.cacheLinks[key]; delete self.topology.nodes[id]; for(var i=0;i<16384;i++) { if(self.topology.slots[i] === id) { delete self.topology.slots[i]; } } var keys = Object.keys(queue); keys.reverse(); for(var i=0;i<keys.length;i++) { self.queue.unshift(queue[keys[i]]); } self.waitForTopology(); }); link.rawCall(['CLIENT', 'SETNAME', hostname]); return link; }; RedisCluster.prototype.toString = function(){ return 'RedisCluster '+JSON.stringify(this.topology.nodes, null, '\t'); }; RedisCluster.prototype.calcSlot = (function crc16Init(){ var CRC16_TAB = [ // unsigned short CRC16_TAB[] = {...}; 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 ]; function crc16Add(crc,c) { return CRC16_TAB[ ( (crc>>8) ^ c ) & 0xFF ] ^ ( (crc<<8) & 0xFFFF ); }; var BUF_SIZE = 1024; var buf = Buffer.alloc(BUF_SIZE); function utf8BytesLength(string) { var utf8length = 0, c = 0; for (var n = 0; n < string.length; n++) { c = string.charCodeAt(n); if (c < 128) { utf8length++; } else if((c > 127) && (c < 2048)) { utf8length = utf8length+2; } else { utf8length = utf8length+3; } } return utf8length; } return function crc16XModem(str){ var len = utf8BytesLength(str); var b = buf; if(len < BUF_SIZE) { b.write(str, 0, len, 'utf8'); } else { b = new Buffer(str, 'utf8'); } var crc = 0; for(var i = 0; i < len; i++) { crc = crc16Add(crc, b.readUInt8(i)); } return crc % 16384; } })(); RedisCluster.prototype.rawCallAsync = function rawCallAsync(args, options) { let self = this; return new Promise((resolve, reject) => { self.rawCall(args, (err, data) => { if(err) return reject(err); resolve(data); }); }, options); } RedisCluster.prototype.rawCall = function rawCall(args, cb, options) { var self = this; if(!Array.isArray(args)) { throw new Error('First argument must be Array'); } if(!this.connected) { this.queue.push({ args: args, cb: cb }); return this; } if(this.queue.length > 0) { var queue = this.queue; this.queue = []; queue.forEach(function(q){ self.rawCall(q.args, q.cb); }); } var targetSlot = 0; var key, start, end; if(args.length > 1) { key = args[1].toString(); start = key.indexOf('{'); if(start !== -1) { end = key.indexOf('}', start); if(end !== -1 && end - start -1 > 0) { key = key.substr(start+1, end-start-1); } } targetSlot = this.calcSlot(key); } if(typeof options !== 'undefined' && typeof options.targetSlot !== 'undefined') { targetSlot = options.targetSlot; } var targetId = this.topology.slots[targetSlot]; if(typeof targetId === 'undefined' || typeof this.topology.nodes[targetId] === 'undefined') { this.connected = false; //throw new Error('Target slot '+targetSlot+' not found on any node, check your cluster'); this.queue.unshift({ args: args, cb: cb }); this.waitForTopology(); return this; } var link = this.topology.nodes[targetId]; if(this.topology.nodes[targetId].link === null) { this.topology.nodes[targetId].link = this.getRedisClient(this.topology.nodes[targetId].host, this.topology.nodes[targetId].port, this.options.auth, this.options); } link = link.link; var cmdId = self.cmdId++; if(args[0].toUpperCase() === 'HMSET' && typeof args[2] === 'object') { var obj = args.splice(2, 1)[0]; for(var k in obj) { args.push(k); args.push(obj[k]); } } link._queue[cmdId] = { args: args, cb: cb }; link.rawCall(args, onResponse); function onResponse(e, resp, size){ if(typeof size === 'undefined') size = -1; if(link && link._queue) { if(typeof link._queue[cmdId] !== 'undefined') { link._queue[cmdId].cb = null; link._queue[cmdId].args = null; } link._queue[cmdId] = null; delete link._queue[cmdId]; } if(e) { var e = e.toString(); if(e.substr(0,3) === 'ASK') { var target = e.split(' ')[2]; var linkNew = self.getRedisClient(target.split(':')[0], target.split(':')[1], self.options.auth, self.options); linkNew.rawCall(['ASKING']); linkNew.rawCall(args, onResponse); return; } //MOVED 7365 127.0.0.1:7001 if(e.substr(0, 5) === 'MOVED') { //this probably mean that cluster topology is changed self.queue.unshift({ args: args, cb: cb }); self.waitForTopology(); return self; } if(typeof cb !== 'undefined') { cb(e, undefined, size); } else { console.log('Redis cluster, unhandled error', args, e); } return; } if(typeof cb !== 'undefined') { if(args[0].toUpperCase() === 'HMGET' && Array.isArray(resp)) { if(resp.length > 0) { var t = resp; resp = {}; for(var i=2; i<args.length;i++) { resp[args[i]] = t[i-2]; } } else { resp = null; } } else if(args[0].toUpperCase() === 'HGETALL' && Array.isArray(resp)) { if(resp.length > 0) { var t = resp; resp = {}; for(var i=0;i<t.length;i+=2) { resp[t[i]] = t[i+1]; } } else { resp = null; } } cb(null, resp, size); } } return this; }; RedisCluster.prototype.waitForTopology = function waitForTopology(){ var self = this; if(this.renewTopologyInProgress) return; this.connected = false; this.renewTopologyInProgress = true; this.topology.slots = {}; this.topology.nodes = {}; var self = this; var connectStr; var link; init(); function init(){ connectStr = Object.keys(self.cacheLinks)[0]; if(typeof connectStr === 'undefined') { self.emit('error', 'No masters known'); setTimeout(function(){ var first = self.getRedisClient(self.firstLink.host, self.firstLink.port, self.options.auth, self.options); first.on('ready', function(){ self.waitForTopology(); }); self.renewTopologyInProgress = false; }, 1000); return; } link = self.cacheLinks[connectStr]; link.rawCall(['CLUSTER', 'NODES'], onGetTopology); } function onGetTopology(e, nodes) { if(e) { delete self.cacheLinks[connectStr]; self.emit('error', e); setTimeout(init, 1000); return; } self.topology.slots = {}; self.topology.nodes = {}; // console.log(nodes); nodes.split('\n').forEach(parseLine); // console.log(self.topology.nodes); if(Object.keys(self.topology.slots).length !== 16384) { setTimeout(init, 1000); return; } self.connected = true; self.renewTopologyInProgress = false; if(self.queue.length > 0) self.ping(); self.emit('ready'); if(self.initialCallback) { var cb = self.initialCallback; self.initialCallback = null; cb(null, self); } } function parseLine(line) { if(!line) return; var cache = line.split(' '); var id = cache[0]; var linkStr = cache[1].split('@')[0]; var host = cache[1].split(':')[0]; var port = cache[1].split(':')[1].split('@')[0]; var flags = cache[2].split(','); var isConected = cache[7] === 'connected'; self.topology.nodes[id] = { id: id, linkStr: linkStr, flags: flags, host: host, port: port, link: null, isSlave: false }; if(flags.indexOf('fail?') !== -1 || flags.indexOf('fail') !== -1) { delete self.topology.nodes[id]; return; } if(flags.indexOf('myself') !== -1) { self.topology.nodes[id].link = link; } if(flags.indexOf('slave') !== -1) { self.topology.nodes[id].isSlave = true; return; } if(!isConected) return; var from, to; for(var i=8;i<cache.length;i++){ if(cache[i].indexOf('-<-') !== -1) continue; if(cache[i].indexOf('-') === -1) { self.topology.slots[cache[i]] = id; continue; } from = parseInt(cache[i].split('-')[0]); to = parseInt(cache[i].split('-')[1]); for(var n=from;n<=to;n++) { self.topology.slots[n] = id; } } } }; RedisCluster.prototype.end = function end(flush) { flush= flush === true; this.connected = false; this.topology.slots = {}; for(var k in this.cacheLinks) { this.cacheLinks[k].end(flush); } this.cacheLinks = {}; this.topology.nodes = {}; }; function redisCmdWrap(cmd) { return function redisCmd() { var args = Array.prototype.slice.call(arguments, 0); args.unshift(cmd); var cb = undefined; if(typeof args[args.length - 1] === 'function') { cb = args.pop(); } if(cmd === 'hmset' && typeof args[args.length - 1] === 'object') { var o = args.pop(); for(var k in o) { args.push(k); args.push(o[k]); } } return this.rawCall(args, cb); } }; redisCommands.forEach(function(cmd){ RedisCluster.prototype[cmd] = redisCmdWrap(cmd); }); module.exports = { RedisCluster: RedisCluster, clusterClient: { clusterInstance: function(options, cb) { var firstLink = { host: '127.0.0.1', port: 7001 }; var settings = { auth: null }; if(typeof options === 'string') { firstLink.host = options.split(':')[0]; firstLink.port = parseInt(options.split(':')[1]); } else if(typeof options === 'object' && !Array.isArray(options)) { for(var k in options) { settings[k] = options[k]; } firstLink = { host: options.host, port: options.port }; return new RedisCluster(firstLink, settings, cb); } else { return cb('Unsupported options'); } return new RedisCluster(firstLink, settings, cb); } } };