Skip to content

Instantly share code, notes, and snippets.

@tangxinfa
Last active November 7, 2016 08:06
Show Gist options
  • Save tangxinfa/3361a11acf2270e8388b43bfcb25ce0e to your computer and use it in GitHub Desktop.
Save tangxinfa/3361a11acf2270e8388b43bfcb25ce0e to your computer and use it in GitHub Desktop.
Connect redis with Minimum Distance First(MDF) algorithm.
var Redis = require('ioredis'),
merge = require('utils-merge'),
_ = require('lodash'),
os = require('os'),
net = require('net'),
util = require('util'),
EventEmitter = require('events').EventEmitter;
/**
* Connect redis with Minimum Distance First(MDF) algorithm.
*
* @param options options.
* - redis
* redis options
* - local_master
* optional, connect to local master if no local slave exists, default is false.
* - minimum_distance
* optional, minimum distance, default is 1.
* - ip_distance
* optional, function (ip) for calculate ip distance, default is MDFRedis.ip_distance.
*/
function MDFRedis(options) {
if (!(this instanceof MDFRedis)) {
return new MDFRedis(arguments[0]);
}
EventEmitter.call(this);
this.options = {
redis: options.redis,
local_master: options.local_master || false,
minimum_distance: options.minimum_distance || 1,
ip_distance: options.ip_distance || MDFRedis.ip_distance
};
if (this.options.redis.sentinels && this.options.redis.role == 'slave') {
// prefer connect to local slave.
this._slave = new Redis(merge({
preferredSlaves: this._closest.bind(this)
}, this.options.redis));
if (this.options.local_master) {
// try master if slave is remote.
this._slave.once('connect', this._slave_connected.bind(this));
} else {
this._redis = this._slave;
}
} else {
// Minimum Distance First(MDF) algorithm only available when connect slave redis through sentinels.
this._redis = new Redis(this.options.redis);
}
}
util.inherits(MDFRedis, EventEmitter);
/**
* Get the connected redis instance.
*
* @param callback function (err, client) called when done.
*
*/
MDFRedis.prototype.redis = function (callback) {
if (this._redis) {
return callback(null, this._redis);
}
var self = this;
this.once('redis', function () {
callback(null, self._redis);
});
};
/**
* Choose closest slave from slaves.
*
* @param slaves All available slaves.
*
* @return closed one, null if no closed slave exists.
*/
MDFRedis.prototype._closest = function (slaves) {
var closest = Number.MAX_VALUE;
var orderedSlaves = [];
for(var i = 0; i < slaves.length; ++i) {
if (net.isIPv4(slaves[i].host)) {
var distance = this.options.ip_distance(slaves[i].host);
if (! orderedSlaves[distance]) {
orderedSlaves[distance] = [];
}
orderedSlaves[distance].push(slaves[i]);
if (distance < closest) {
closest = distance;
}
}
}
return closest !== Number.MAX_VALUE && closest <= this.options.minimum_distance && _.sample(orderedSlaves[closest]);
};
/// Called when slave connected.
MDFRedis.prototype._slave_connected = function () {
if (net.isIPv4(this._slave.stream.remoteAddress)) {
if (this.options.ip_distance(this._slave.stream.remoteAddress) > this.options.minimum_distance) {
// local slave not exists, try master.
var options = merge({}, this.options.redis);
options.role = 'master';
this._master = new Redis(options);
this._master.once('connect', this._master_connected.bind(this));
return;
}
}
// local slave exists, use it.
this._redis = this._slave;
this.emit('redis');
};
/// Called when master connected.
MDFRedis.prototype._master_connected = function () {
if (net.isIPv4(this._master.stream.remoteAddress) && this.options.ip_distance(this._master.stream.remoteAddress) <= this.options.minimum_distance) {
// local master exists, use it.
this._redis = this._master;
this._slave.disconnect();
delete this._slave;
} else {
// local master not exists, use remote slave.
this._redis = this._slave;
this._master.disconnect();
delete this._master;
}
this.emit('redis');
};
/**
* Get all local network addresses.
*
*
* @return local network addresses.
*/
MDFRedis.local_addresses = function () {
if (! MDFRedis._local_addresses) {
MDFRedis._local_addresses = [];
var interfaces = os.networkInterfaces();
var keys = Object.keys(interfaces).filter(function(key) {
var items = interfaces[key]
for(var i = 0; i < items.length; ++i) {
if (items[i].family === 'IPv4') {
MDFRedis._local_addresses.push(items[i].address);
}
}
});
}
return MDFRedis._local_addresses;
};
/// Calculate ip distance.
MDFRedis.ip_distance = function (ip) {
var min_distance = 4;
var ip_segments = ip.split('.');
var local_addresses = MDFRedis.local_addresses();
for(var i = 0; i < local_addresses.length; ++i) {
var local_ip_segments = local_addresses[i].split('.');
var j = 0;
while(j < 4) {
if (ip_segments[j] == local_ip_segments[j]) {
++j;
continue;
}
break;
}
if ((4 - j) < min_distance) {
min_distance = (4 - j);
}
}
return min_distance;
};
module.exports = MDFRedis;
var MDFRedis = require(__dirname + '/MDFRedis.js');
var redis = new MDFRedis({
redis: {
name: 'store',
sentinels: [ { host:'127.0.0.1', port: 16380 }, { host:'127.0.0.1', port: 16381 }, { host:'127.0.0.1', port: 16382 } ],
role: 'slave',
db: 0
},
local_master: true
});
redis.redis(function (err, client) {
console.log("connected");
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment