Comparing version 1.2.3 to 1.2.4
77
API.md
@@ -5,2 +5,4 @@ ## Classes | ||
<dd></dd> | ||
<dt><a href="#Cluster">Cluster</a> ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code></dt> | ||
<dd></dd> | ||
<dt><a href="#Commander">Commander</a></dt> | ||
@@ -14,2 +16,5 @@ <dd></dd> | ||
</dd> | ||
<dt><a href="#defaultOptions">defaultOptions</a></dt> | ||
<dd><p>Default options</p> | ||
</dd> | ||
</dl> | ||
@@ -179,2 +184,68 @@ <a name="Redis"></a> | ||
**Kind**: static method of <code>[Redis](#Redis)</code> | ||
<a name="Cluster"></a> | ||
## Cluster ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code> | ||
**Kind**: global class | ||
**Extends:** <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>, <code>[Commander](#Commander)</code> | ||
* [Cluster](#Cluster) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code> | ||
* [new Cluster(startupNodes, options)](#new_Cluster_new) | ||
* [.disconnect()](#Cluster#disconnect) | ||
* [.getBuiltinCommands()](#Commander#getBuiltinCommands) ⇒ <code>Array.<string></code> | ||
* [.createBuiltinCommand(commandName)](#Commander#createBuiltinCommand) ⇒ <code>object</code> | ||
* [.defineCommand(name, definition)](#Commander#defineCommand) | ||
<a name="new_Cluster_new"></a> | ||
### new Cluster(startupNodes, options) | ||
Creates a Redis Cluster instance | ||
| Param | Type | Default | Description | | ||
| --- | --- | --- | --- | | ||
| startupNodes | <code>Array.<Object></code> | | An array of nodes in the cluster, [{ port: number, host: string }] | | ||
| options | <code>Object</code> | | | | ||
| [options.enableOfflineQueue] | <code>boolean</code> | <code>true</code> | See Redis class | | ||
| [options.lazyConnect] | <code>boolean</code> | <code>false</code> | See Redis class | | ||
| [options.maxRedirections] | <code>number</code> | <code>16</code> | 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. | | ||
| [options.clusterRetryStrategy] | <code>function</code> | | See "Quick Start" section | | ||
| [options.retryDelayOnFailover] | <code>number</code> | <code>2000</code> | When an error is received when sending a command(e.g. "Connection is closed." when the target Redis node is down), | | ||
| [options.retryDelayOnClusterDown] | <code>number</code> | <code>1000</code> | When a CLUSTERDOWN error is received, client will retry if `retryDelayOnClusterDown` is valid delay time. | | ||
<a name="Cluster#disconnect"></a> | ||
### cluster.disconnect() | ||
Disconnect from every node in the cluster. | ||
**Kind**: instance method of <code>[Cluster](#Cluster)</code> | ||
**Access:** public | ||
<a name="Commander#getBuiltinCommands"></a> | ||
### cluster.getBuiltinCommands() ⇒ <code>Array.<string></code> | ||
Return supported builtin commands | ||
**Kind**: instance method of <code>[Cluster](#Cluster)</code> | ||
**Returns**: <code>Array.<string></code> - command list | ||
**Access:** public | ||
<a name="Commander#createBuiltinCommand"></a> | ||
### cluster.createBuiltinCommand(commandName) ⇒ <code>object</code> | ||
Create a builtin command | ||
**Kind**: instance method of <code>[Cluster](#Cluster)</code> | ||
**Returns**: <code>object</code> - functions | ||
**Access:** public | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| commandName | <code>string</code> | command name | | ||
<a name="Commander#defineCommand"></a> | ||
### cluster.defineCommand(name, definition) | ||
Define a custom command using lua script | ||
**Kind**: instance method of <code>[Cluster](#Cluster)</code> | ||
| Param | Type | Default | Description | | ||
| --- | --- | --- | --- | | ||
| name | <code>string</code> | | the command name | | ||
| definition | <code>object</code> | | | | ||
| definition.lua | <code>string</code> | | the lua code | | ||
| [definition.numberOfKeys] | <code>number</code> | <code></code> | the number of keys. If omit, you have to pass the number of keys as the first argument every time you invoke the command | | ||
<a name="Commander"></a> | ||
@@ -237,1 +308,7 @@ ## Commander | ||
**Access:** protected | ||
<a name="defaultOptions"></a> | ||
## defaultOptions | ||
Default options | ||
**Kind**: global variable | ||
**Access:** protected |
@@ -5,2 +5,8 @@ ## Changelog | ||
### v1.2.4 - May 9, 2015 | ||
* Try a random node when the target slot isn't served by the cluster. | ||
* Remove `refreshAfterFails` option. | ||
* Try random node when refresh slots. | ||
### v1.2.3 - May 9, 2015 | ||
@@ -7,0 +13,0 @@ |
@@ -19,4 +19,2 @@ 'use strict'; | ||
* @param {boolean} [options.lazyConnect=false] - See Redis class | ||
* @param {number} [options.refreshAfterFails=4] - When `MOVED` error is received more times than `refreshAfterFails`, client will call CLUSTER SLOTS | ||
command to refresh the slot cache. | ||
* @param {number} [options.maxRedirections=16] - When a MOVED or ASK error is received, client will redirect the | ||
@@ -42,3 +40,2 @@ * command to another node. This option limits the max redirections allowed to send a command. | ||
this.connections = {}; | ||
this.fails = 0; | ||
this.retryAttempts = 0; | ||
@@ -58,3 +55,2 @@ this.options = _.defaults(options || {}, this.options || {}, Cluster.defaultOptions); | ||
Cluster.defaultOptions = _.assign({}, Redis.defaultOptions, { | ||
refreshAfterFails: 4, | ||
maxRedirections: 16, | ||
@@ -75,16 +71,10 @@ retryDelayOnFailover: 2000, | ||
var _this = this; | ||
this.startupNodes.forEach(function (options) { | ||
_this.createNode(options.port, options.host); | ||
this.once('refresh', function () { | ||
_this.setStatus('connect'); | ||
}); | ||
this.refreshSlotsCache(function (err) { | ||
if (err && _this.status !== 'end') { | ||
// Failed to refresh slots, try to reconnect | ||
_this.disconnect(true); | ||
} else if (!err) { | ||
_this.retryAttempts = 0; | ||
_this.manuallyClosing = false; | ||
_this.setStatus('connect'); | ||
_this.executeOfflineCommands(); | ||
_this.setStatus('ready'); | ||
} | ||
this.once('connect', function () { | ||
_this.retryAttempts = 0; | ||
_this.manuallyClosing = false; | ||
_this.setStatus('ready'); | ||
_this.executeOfflineCommands(); | ||
}); | ||
@@ -106,2 +96,7 @@ this.once('end', function () { | ||
}); | ||
this.startupNodes.forEach(function (options) { | ||
_this.createNode(options.port, options.host); | ||
}); | ||
this.refreshSlotsCache(); | ||
}; | ||
@@ -160,3 +155,8 @@ | ||
if (this.isRefreshing) { | ||
return setTimeout(callback, 50); | ||
if (typeof callback === 'function') { | ||
process.nextTick(function () { | ||
callback(); | ||
}); | ||
} | ||
return; | ||
} | ||
@@ -171,3 +171,3 @@ this.isRefreshing = true; | ||
var keys = Object.keys(this.nodes); | ||
var keys = _.shuffle(Object.keys(this.nodes)); | ||
@@ -189,2 +189,3 @@ var _this = this; | ||
} else { | ||
_this.emit('refresh'); | ||
wrapper(); | ||
@@ -245,10 +246,4 @@ } | ||
_this.slots[errv[1]] = node; | ||
if (++_this.fails < _this.options.refreshAfterFails) { | ||
tryConnection(); | ||
} else { | ||
_this.fails = 0; | ||
_this.refreshSlotsCache(function () { | ||
tryConnection(); | ||
}); | ||
} | ||
tryConnection(); | ||
_this.refreshSlotsCache(); | ||
} else { | ||
@@ -283,12 +278,17 @@ debug('command %s is required to ask %s:%s', command.name, hostPort[0], hostPort[1]); | ||
function tryConnection (random, asking) { | ||
var redis; | ||
if (random || typeof targetSlot !== 'number') { | ||
redis = _this.nodes[_.sample(Object.keys(_this.nodes))]; | ||
} else if (asking) { | ||
redis = asking; | ||
redis.asking(); | ||
} else { | ||
redis = _this.slots[targetSlot]; | ||
if (_this.status === 'end') { | ||
command.reject(new Error('Cluster is ended.')); | ||
return; | ||
} | ||
if (redis) { | ||
if (_this.status === 'ready') { | ||
var redis; | ||
if (typeof targetSlot === 'number') { | ||
redis = _this.slots[targetSlot]; | ||
} else if (asking && !random) { | ||
redis = asking; | ||
redis.asking(); | ||
} | ||
if (random || typeof targetSlot !== 'number' || !redis) { | ||
redis = _this.nodes[_.sample(Object.keys(_this.nodes))]; | ||
} | ||
redis.sendCommand(command, stream); | ||
@@ -302,3 +302,3 @@ } else if (_this.options.enableOfflineQueue) { | ||
} else { | ||
command.reject(new Error('Cluster isn\'t connected and enableOfflineQueue options is false')); | ||
command.reject(new Error('Cluster isn\'t ready and enableOfflineQueue options is false')); | ||
} | ||
@@ -345,2 +345,37 @@ } | ||
var multi = Cluster.prototype.multi; | ||
Cluster.prototype.multi = function (options) { | ||
if (options && options.pipeline === false) { | ||
return multi.call(this, options); | ||
} | ||
var ttl = this.options.maxRedirections; | ||
var pipeline = multi.call(this, options); | ||
var exec = pipeline.exec; | ||
pipeline.exec = function (callback) { | ||
var sampleKey; | ||
for (var i = 0; i < this._queue.length; ++i) { | ||
sampleKey = this._queue[i].getKeys()[0]; | ||
if (sampleKey) { | ||
break; | ||
} | ||
} | ||
this.slot = sampleKey ? utils.calcSlot(sampleKey) : Math.random() * 16384 | 0; | ||
this.targetRedis = this.redis.slots[this.slot]; | ||
var promise = exec.call(pipeline).catch(function (err) { | ||
console.log(err.message); | ||
// if (err.message ) | ||
ttl -= 1; | ||
if (ttl <= 0) { | ||
throw new Error('Too many Cluster redirections. Last error: ' + err); | ||
} | ||
return pipeline.exec(); | ||
}); | ||
return promise.nodeify(callback); | ||
}; | ||
return pipeline; | ||
}; | ||
module.exports = Cluster; |
'use strict'; | ||
var _ = require('lodash'); | ||
var Command = require('./command'); | ||
var Commander = require('./commander'); | ||
@@ -10,6 +9,10 @@ var fbuffer = require('flexbuffer'); | ||
function Pipeline(redis) { | ||
function Pipeline(redis, options) { | ||
Commander.call(this); | ||
if (!options) { | ||
options = {}; | ||
} | ||
this.redis = redis; | ||
this.targetRedis = redis; | ||
this._queue = []; | ||
@@ -70,12 +73,6 @@ this._result = []; | ||
} | ||
var isCluster = this.redis.constructor.name === 'Cluster'; | ||
var sampleKey; | ||
// Check whether scripts exists and get a sampleKey. | ||
// Check whether scripts exists | ||
var scripts = []; | ||
for (var i = 0; i < this._queue.length; ++i) { | ||
var item = this._queue[i]; | ||
if (isCluster && !sampleKey) { | ||
sampleKey = item.getKeys()[0]; | ||
} | ||
if (item.name !== 'evalsha') { | ||
@@ -90,10 +87,2 @@ continue; | ||
} | ||
var sampleSlot; | ||
if (isCluster) { | ||
if (sampleKey) { | ||
sampleSlot = utils.calcSlot(sampleKey); | ||
} else { | ||
sampleSlot = Math.random() * 16384 | 0; | ||
} | ||
} | ||
@@ -103,9 +92,3 @@ var promise; | ||
if (scripts.length) { | ||
var redis; | ||
if (isCluster) { | ||
redis = this.redis.slots[sampleSlot]; | ||
} else { | ||
redis = this.redis; | ||
} | ||
promise = redis.script('exists', scripts.map(function (item) { | ||
promise = this.targetRedis.script('exists', scripts.map(function (item) { | ||
return item.sha; | ||
@@ -120,3 +103,3 @@ })).then(function (results) { | ||
return Promise.all(pending.map(function (script) { | ||
return redis.script('load', script.lua); | ||
return _this.redis.script('load', script.lua); | ||
})); | ||
@@ -152,7 +135,3 @@ }); | ||
} | ||
if (isCluster) { | ||
_this.redis.slots[sampleSlot].stream.write(data); | ||
} else { | ||
_this.redis.stream.write(data); | ||
} | ||
_this.targetRedis.stream.write(data); | ||
} | ||
@@ -163,3 +142,3 @@ } | ||
for (var i = 0; i < _this._queue.length; ++i) { | ||
_this.redis.sendCommand(_this._queue[i], stream, sampleSlot); | ||
_this.redis.sendCommand(_this._queue[i], stream, _this.slot); | ||
} | ||
@@ -166,0 +145,0 @@ return _this.promise; |
{ | ||
"name": "ioredis", | ||
"version": "1.2.3", | ||
"version": "1.2.4", | ||
"description": "A delightful, performance-focused Redis client for Node and io.js", | ||
@@ -9,3 +9,3 @@ "main": "index.js", | ||
"test:cov": "NODE_ENV=test DEBUG=ioredis:* node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec", | ||
"generate-docs": "jsdoc2md lib/redis.js lib/redis_cluster.js lib/commander.js > API.md", | ||
"generate-docs": "jsdoc2md lib/redis.js lib/cluster.js lib/commander.js > API.md", | ||
"bench": "matcha benchmarks/*.js" | ||
@@ -12,0 +12,0 @@ }, |
@@ -494,4 +494,2 @@ # ioredis | ||
* `refreshAfterFails`: When `MOVED` errors are received more times than `refreshAfterFails`, client will call CLUSTER SLOTS | ||
command to refresh the slot cache. The default value is `4`. | ||
* `maxRedirections`: When a `MOVED` or `ASK` error is received, client will redirect the | ||
@@ -498,0 +496,0 @@ command to another node. This option limits the max redirections allowed when sending a command. The default value is `16`. |
@@ -113,9 +113,11 @@ 'use strict'; | ||
var times = 0; | ||
var slotTable = [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
var node1 = new MockServer(30001, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
} else if (argv[0] === 'get' && argv[1] === 'foo') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
if (times++ === 1) { | ||
@@ -131,2 +133,5 @@ expect(moved).to.eql(true); | ||
var node2 = new MockServer(30002, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
@@ -146,2 +151,42 @@ expect(moved).to.eql(false); | ||
}); | ||
it('should auto redirect the command within a pipeline', function (done) { | ||
var moved = false; | ||
var times = 0; | ||
var slotTable = [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
var node1 = new MockServer(30001, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
if (times++ === 1) { | ||
expect(moved).to.eql(true); | ||
process.nextTick(function () { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
} | ||
} | ||
}); | ||
var node2 = new MockServer(30002, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
expect(moved).to.eql(false); | ||
moved = true; | ||
return new Error('MOVED ' + utils.calcSlot('foo') + ' 127.0.0.1:30001'); | ||
} | ||
}); | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' } | ||
], { lazyConnect: false }); | ||
cluster.get('foo', function () { | ||
cluster.get('foo'); | ||
}); | ||
}); | ||
}); | ||
@@ -153,9 +198,11 @@ | ||
var times = 0; | ||
var slotTable = [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
var node1 = new MockServer(30001, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
} else if (argv[0] === 'get' && argv[1] === 'foo') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
expect(asked).to.eql(true); | ||
@@ -167,2 +214,5 @@ } else if (argv[0] === 'asking') { | ||
var node2 = new MockServer(30002, function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
return slotTable; | ||
} | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
@@ -169,0 +219,0 @@ if (++times === 2) { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
213191
4649
691