Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ReadOnly mode for Standalone/Cluster mode #69

Merged
merged 2 commits into from
Jun 11, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions lib/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var Command = require('./command');
* @param {Object} options
* @param {boolean} [options.enableOfflineQueue=true] - See Redis class
* @param {boolean} [options.lazyConnect=false] - See Redis class
* @param {boolean} [options.readOnly=false] - Connect in READONLY mode
* @param {number} [options.maxRedirections=16] - When a MOVED or ASK error is received, client will redirect the
* command to another node. This option limits the max redirections allowed to send a command.
* @param {function} [options.clusterRetryStrategy] - See "Quick Start" section
Expand All @@ -36,6 +37,7 @@ function Cluster (startupNodes, options) {
this.startupNodes = startupNodes;

this.nodes = {};
this.masterNodes = {};
this.slots = [];
this.connections = {};
this.retryAttempts = 0;
Expand All @@ -57,6 +59,7 @@ Cluster.defaultOptions = _.assign({}, Redis.defaultOptions, {
maxRedirections: 16,
retryDelayOnFailover: 2000,
retryDelayOnClusterDown: 1000,
readOnly: false,
clusterRetryStrategy: function (times) {
return Math.min(100 + times * 2, 2000);
}
Expand Down Expand Up @@ -172,13 +175,22 @@ Cluster.prototype.createNode = function (port, host) {
}
});
}

return this.nodes[key];
};

Cluster.prototype.selectRandomMasterNode = function () {
return this.nodes[_.sample(Object.keys(this.masterNodes))];
};

Cluster.prototype.selectRandomNode = function () {
return this.nodes[_.sample(Object.keys(this.nodes))];
};

Cluster.prototype.selectRandomNodeForSlot = function (targetSlot) {
return _.sample(this.slots[targetSlot].allNodes);
};

Cluster.prototype.selectSubscriber = function () {
if (Object.keys(this.nodes).length === 0) {
this.subscriber = null;
Expand Down Expand Up @@ -284,7 +296,7 @@ Cluster.prototype.sendCommand = function (command, stream, node) {
_this.handleError(err, ttl, {
moved: function (node, slot, hostPort) {
debug('command %s is moved to %s:%s', command.name, hostPort[0], hostPort[1]);
_this.slots[slot] = node;
_this.slots[slot].masterNode = node;
tryConnection();
_this.refreshSlotsCache();
},
Expand Down Expand Up @@ -319,14 +331,18 @@ Cluster.prototype.sendCommand = function (command, stream, node) {
redis = _this.subscriber;
} else {
if (typeof targetSlot === 'number') {
redis = _this.slots[targetSlot];
if (_this.options.readOnly) {
redis = _this.selectRandomNodeForSlot(targetSlot);
} else {
redis = _this.slots[targetSlot].masterNode;
}
}
if (asking && !random) {
redis = asking;
redis.asking();
}
if (random || !redis) {
redis = _this.selectRandomNode();
redis = _this.selectRandomMasterNode();
}
}
if (node && !node.redis) {
Expand Down Expand Up @@ -394,20 +410,32 @@ Cluster.prototype.getInfoFromNode = function (redis, callback) {
}
var i;
var oldNodes = {};
var allNodes = [];
var keys = Object.keys(_this.nodes);
for (i = 0; i < keys.length; ++i) {
oldNodes[keys[i]] = true;
}
for (i = 0; i < result.length; ++i) {
var item = result[i];
var host = item[2][0];
var port = item[2][1];
var node = _this.createNode(port, host);
delete oldNodes[host + ':' + port];
for (var slot = item[0]; slot <= item[1]; ++slot) {
_this.slots[slot] = node;
}
var items = result[i];
var slotRangeStart = items.shift();
var slotRangeEnd = items.shift();
var master = items.shift();
var masterNodeKey = master[0] + ':' + master[1];
var masterNode = _this.createNode(master[1], master[0]);
_this.masterNodes[masterNodeKey] = masterNode;
allNodes.push(masterNode);
delete oldNodes[masterNodeKey];
items.forEach(function(item) {
var host = item[0];
var port = item[1];
allNodes.push(_this.createNode(port, host));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to connect to the slaves when readOnly is disabled.

delete oldNodes[host + ':' + port];
});
for (var slot = slotRangeStart; slot <= slotRangeEnd; ++slot) {
_this.slots[slot] = { masterNode : masterNode, allNodes: allNodes };
};
}

Object.keys(oldNodes).forEach(function (key) {
_this.nodes[key].disconnect();
});
Expand Down
13 changes: 12 additions & 1 deletion lib/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ try {
* @param {number} [options.port=6379] - Port of the Redis server.
* @param {string} [options.host=localhost] - Host of the Redis server.
* @param {string} [options.family=4] - Version of IP stack. Defaults to 4.
* @param {boolean} [options.readOnly=false] - Connect in READONLY mode
* @param {string} [options.path=null] - Local domain socket path. If set the `port`, `host`
* and `family` will be ignored.
* @param {string} [options.password=null] - If set, client will send AUTH command
Expand Down Expand Up @@ -163,7 +164,8 @@ Redis.defaultOptions = {
enableReadyCheck: true,
autoResubscribe: true,
autoResendUnfulfilledCommands: true,
lazyConnect: false
lazyConnect: false,
readOnly: false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since readOnly only works in cluster mode, we'd better only have it in cluster.js and listen to the ready event in the createNode method of cluster.js

};

Redis.prototype.parseOptions = function () {
Expand Down Expand Up @@ -257,6 +259,15 @@ Redis.prototype.connect = function (callback) {
_this.removeListener('connect', connectionConnectHandler);
reject(err);
};
var readOnlyHandler = function() {
debug('Sending readonly command');
_this.readonly();
};

if (_this.options.readOnly) {
_this.once('ready', readOnlyHandler);
}

_this.once('connect', connectionConnectHandler);
_this.once('close', connectionCloseHandler);
});
Expand Down
55 changes: 55 additions & 0 deletions test/functional/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,61 @@ describe('cluster', function () {
});
});
});

describe('readonly', function() {
it('should connect all nodes and issue a readonly', function (done) {
var setReadOnlyNode1 = false;
var setReadOnlyNode2 = false;
var setReadOnlyNode3 = false;
var slotTable = [
[0, 5460, ['127.0.0.1', 30001]],
[5461, 10922, ['127.0.0.1', 30002]],
[10923, 16383, ['127.0.0.1', 30003]]
];
var node1 = new MockServer(30001, function (argv) {
if (argv[0] === 'cluster' && argv[1] === 'slots') {
return slotTable;
}
if (argv[0] === 'readonly') {
setReadOnlyNode1 = true;
expect(setReadOnlyNode1).to.eql(true);
return 'OK';
}
});
var node2 = new MockServer(30002, function (argv) {
if (argv[0] === 'cluster' && argv[1] === 'slots') {
return slotTable;
}
if (argv[0] === 'readonly') {
setReadOnlyNode2 = true;
expect(setReadOnlyNode2).to.eql(true);
return 'OK'
}
});

var node3 = new MockServer(30003, function (argv) {
if (argv[0] === 'cluster' && argv[1] === 'slots') {
return slotTable;
}
if (argv[0] === 'readonly') {
setReadOnlyNode3 = true;
expect(setReadOnlyNode3).to.eql(true);
return 'OK'
}
});

var cluster = new Redis.Cluster([
{ host: '127.0.0.1', port: '30001'}],
{ readOnly: true }
);
cluster.on('ready', function() {
expect(setReadOnlyNode1 || setReadOnlyNode2 || setReadOnlyNode3).to.eql(true);
done();
});

});
});

});

function disconnect (clients, callback) {
Expand Down